001/*
002 *
003 * Copyright (C) 2017 Ping Identity Corporation
004 *       All rights reserved.
005 *
006 * The contents of this file are the property of Ping Identity Corporation.
007 * You may not copy or use this file, in either source code or executable
008 * form, except in compliance with terms set by Ping Identity Corporation.
009 * For further information please contact:
010 *
011 *       Ping Identity Corporation
012 *       1099 18th St Suite 2950
013 *       Denver, CO 80202
014 *       303.468.2900
015 *       http://www.pingidentity.com
016 *
017 */
018package com.pingidentity.ds.plugin;
019
020import com.unboundid.directory.sdk.common.internal.Reconfigurable;
021import com.unboundid.directory.sdk.common.operation.AddRequest;
022import com.unboundid.directory.sdk.common.operation.SearchRequest;
023import com.unboundid.directory.sdk.common.operation.*;
024import com.unboundid.directory.sdk.common.types.ActiveOperationContext;
025import com.unboundid.directory.sdk.common.types.ActiveSearchOperationContext;
026import com.unboundid.directory.sdk.common.types.LogSeverity;
027import com.unboundid.directory.sdk.common.types.UpdatableEntry;
028import com.unboundid.directory.sdk.ds.api.Plugin;
029import com.unboundid.directory.sdk.ds.config.PluginConfig;
030import com.unboundid.directory.sdk.ds.types.DirectoryServerContext;
031import com.unboundid.directory.sdk.ds.types.PostOperationPluginResult;
032import com.unboundid.directory.sdk.ds.types.PreParsePluginResult;
033import com.unboundid.directory.sdk.ds.types.SearchEntryPluginResult;
034import com.unboundid.ldap.sdk.*;
035import com.unboundid.ldap.sdk.ExtendedResult;
036import com.unboundid.ldap.sdk.unboundidds.extensions.PasswordPolicyStateExtendedRequest;
037import com.unboundid.ldap.sdk.unboundidds.extensions.PasswordPolicyStateExtendedResult;
038import com.unboundid.ldap.sdk.unboundidds.extensions.PasswordPolicyStateOperation;
039import com.unboundid.util.StaticUtils;
040import com.unboundid.util.args.*;
041
042import java.text.ParseException;
043import java.text.SimpleDateFormat;
044import java.util.*;
045import java.util.concurrent.CopyOnWriteArrayList;
046
047import static com.unboundid.ldap.sdk.unboundidds.extensions.PasswordPolicyStateOperation.*;
048
049
050/**
051 * This class provides an implementation of a Server SDK plugin that can be
052 * used to interact with password policy state without having recourse to using
053 * the PasswordPolicyStateExtendedRequest
054 * <p>
055 * This might be useful for developers interacting with our products through third-party
056 * LDAP libraries without extended operation support altogether, like LINQ to LDAP for .Net
057 */
058public class PwpUserState extends Plugin implements Reconfigurable<PluginConfig>
059{
060    
061    /**
062     * The name of the argument used to specify the SimpleDateFormat date format to use
063     * when parsing provided dates
064     */
065    private static final String ARG_TIME_FORMAT = "time-format";
066    
067    /**
068     * The default value for the date format
069     */
070    private static final String ARG_TIME_FORMAT_DEFAULT = "yyyyMMddHHmmss.SSS'Z'";
071    
072    
073    /**
074     * The consistent attribute prefix
075     */
076    private static final String ATTR_EXTENSION = "ds-pwp-user-state";
077    private static final String ATTR_SEPARATOR = "-";
078    private static final String ATTR_PREFIX = ATTR_EXTENSION + ATTR_SEPARATOR;
079    
080    /**
081     * The wildcard attribute to retrieve all available password policy account state attributes
082     */
083    private static final String ATTR_GET_ALL = ATTR_PREFIX + "get-all";
084    
085    /**
086     * The attribute to get or set the account activation time
087     */
088    private static final String ATTR_ACCOUNT_ACTIVATION_TIME = ATTR_PREFIX + "account-activation-time";
089    
090    /**
091     * The attribute to get or set the account disabled state
092     */
093    private static final String ATTR_ACCOUNT_DISABLED = ATTR_PREFIX + "account-disabled";
094    
095    /**
096     * The attribute to get, set or clear the account expiration time
097     */
098    private static final String ATTR_ACCOUNT_EXPIRATION_TIME = ATTR_PREFIX + "account-expiration-time";
099    
100    /**
101     * The attribute to get whether the account has expired
102     */
103    private static final String ATTR_ACCOUNT_EXPIRED = ATTR_PREFIX + "account-expired";
104    
105    /**
106     * The attribute to get whether the account is locked due to excessive authentication failures
107     */
108    private static final String ATTR_ACCOUNT_FAILURE_LOCKED = ATTR_PREFIX + "account-failure-locked";
109    
110    /**
111     * The attribute to get whether the account is locked due to excessive idle time
112     */
113    private static final String ATTR_ACCOUNT_IDLE_LOCKED = ATTR_PREFIX + "account-idle-locked";
114    
115    /**
116     *
117     */
118    private static final String ATTR_ACCOUNT_NOT_ACTIVE_YET = ATTR_PREFIX + "account-not-active-yet";
119    private static final String ATTR_ACCOUNT_RESET_LOCKED = ATTR_PREFIX + "account-reset-locked";
120    private static final String ATTR_ACCOUNT_USABILITY_ERROR = ATTR_PREFIX + "account-usability-error";
121    private static final String ATTR_ACCOUNT_USABILITY_NOTICE = ATTR_PREFIX + "account-usability-notice";
122    private static final String ATTR_ACCOUNT_USABILITY_WARNING = ATTR_PREFIX + "account-usability-warning";
123    private static final String ATTR_ACCOUNT_USABLE = ATTR_PREFIX + "account-usable";
124    private static final String ATTR_AUTH_FAILURE_TIME = ATTR_PREFIX + "auth-failure-time";
125    private static final String ATTR_AVAILABLE_SASL_MECHANISM = ATTR_PREFIX + "available-sasl-mechanism";
126    private static final String ATTR_AVAILABLE_TOTP_DELIVERY_MECHANISM = ATTR_PREFIX +
127            "available-totp-delivery-mechanism";
128    private static final String ATTR_FAILURE_LOCKOUT_TIME = ATTR_PREFIX + "failure-lockout-time";
129    private static final String ATTR_GRACE_LOGIN_USE_TIME = ATTR_PREFIX + "grace-login-use-time";
130    private static final String ATTR_IDLE_LOCKOUT_TIME = ATTR_PREFIX + "idle-lockout-time";
131    private static final String ATTR_LAST_LOGIN_IP_ADDRESS = ATTR_PREFIX + "last-login-ip-address";
132    private static final String ATTR_LAST_LOGIN_TIME = ATTR_PREFIX + "last-login-time";
133    private static final String ATTR_PW_CHANGED_BY_REQUIRED_TIME = ATTR_PREFIX + "pw-changed-by-required-time";
134    private static final String ATTR_PW_CHANGED_TIME = ATTR_PREFIX + "pw-changed-time";
135    private static final String ATTR_PW_EXPIRATION_TIME = ATTR_PREFIX + "pw-expiration-time";
136    private static final String ATTR_PW_EXPIRATION_WARNED_TIME = ATTR_PREFIX + "pw-expiration-warned-time";
137    private static final String ATTR_PW_EXPIRED = ATTR_PREFIX + "pw-expired";
138    private static final String ATTR_PW_HISTORY = ATTR_PREFIX + "pw-history";
139    private static final String ATTR_PW_HISTORY_COUNT = ATTR_PREFIX + "pw-history-count";
140    private static final String ATTR_PW_RESET = ATTR_PREFIX + "pw-reset";
141    private static final String ATTR_PW_RETIRED_TIME = ATTR_PREFIX + "pw-retired-time";
142    private static final String ATTR_PWP_DN = ATTR_PREFIX + "pwp-dn";
143    private static final String ATTR_REMAINING_AUTH_FAILURE_COUNT = ATTR_PREFIX + "remaining-auth-failure-count";
144    private static final String ATTR_REMAINING_GRACE_LOGIN_COUNT = ATTR_PREFIX + "remaining-grace-login-count";
145    //private static final String ATTR_RESET_LOCKOUT_TIME = ATTR_PREFIX + "reset-lockout-time";
146    
147    private static final String ATTR_HAS_RETIRED_PASSWORD = ATTR_PREFIX + "has-retired-password";
148    private static final String ATTR_RETIRED_PASSWORD_EXPIRATION_TIME = ATTR_PREFIX +
149            "retired-password-expiration-time";
150    private static final String ATTR_SECONDS_UNTIL_ACCOUNT_ACTIVATION = ATTR_PREFIX +
151            "seconds-until-account-activation";
152    private static final String ATTR_SECONDS_UNTIL_ACCOUNT_EXPIRATION = ATTR_PREFIX +
153            "seconds-until-account-expiration";
154    private static final String ATTR_SECONDS_UNTIL_AUTH_FAILURE_UNLOCK = ATTR_PREFIX +
155            "seconds-until-auth-failure-unlock";
156    private static final String ATTR_SECONDS_UNTIL_IDLE_LOCKOUT = ATTR_PREFIX + "seconds-until-idle-lockout";
157    private static final String ATTR_SECONDS_UNTIL_PW_EXPIRATION = ATTR_PREFIX + "seconds-until-pw-expiration";
158    private static final String ATTR_SECONDS_UNTIL_PW_EXPIRATION_WARNING = ATTR_PREFIX +
159            "seconds-until-pw-expiration-warning";
160    private static final String ATTR_SECONDS_UNTIL_PW_RESET_LOCKOUT = ATTR_PREFIX + "seconds-until-pw-reset-lockout";
161    private static final String ATTR_SECONDS_UNTIL_REQUIRED_CHANGED_TIME = ATTR_PREFIX +
162            "seconds-until-required-changed-time";
163    
164    private static final String NOW = "now";
165    
166    
167    // The date format to parse provided dates as configured
168    private volatile SimpleDateFormat dateFormatter;
169    
170    // The server context for this instance of the product
171    private volatile DirectoryServerContext serverContext;
172    private HashMap<Integer, String> opsMap;
173    
174    /**
175     * Retrieves the name for this extension.
176     *
177     * @return The name for this extension.
178     */
179    @Override
180    public String getExtensionName()
181    {
182        return "Password Policy User State Plugin";
183    }
184    
185    /**
186     * Retrieves a description for this extension.
187     *
188     * @return A description for this extension.
189     */
190    @Override
191    public String[] getExtensionDescription()
192    {
193        return new String[]{"This extension allows to retrieve or set user state without directly using the " +
194                "PasswordPolicyStateExtendedRequest",
195                "Providing a single trigger attribute (" + ATTR_GET_ALL + ") along with a search request will " +
196                        "retrieve all available " +
197                        "information. " +
198                        "The following password policy state operations are supported:<br/>" +
199                        "<ul>" +
200                        "<li>OP_TYPE_GET_ACCOUNT_ACTIVATION_TIME</li>" +
201                        "<li>OP_TYPE_GET_ACCOUNT_DISABLED_STATE</li>" +
202                        "<li>OP_TYPE_GET_ACCOUNT_EXPIRATION_TIME</li>" +
203                        "<li>OP_TYPE_GET_ACCOUNT_USABILITY_ERRORS</li>" +
204                        "<li>OP_TYPE_GET_ACCOUNT_USABILITY_NOTICES</li>" +
205                        "<li>OP_TYPE_GET_ACCOUNT_USABILITY_WARNINGS</li>" +
206                        "<li>OP_TYPE_GET_AUTH_FAILURE_TIMES</li>" +
207                        "<li>OP_TYPE_GET_GRACE_LOGIN_USE_TIMES</li>" +
208                        "<li>OP_TYPE_GET_LAST_LOGIN_IP_ADDRESS</li>" +
209                        "<li>OP_TYPE_GET_LAST_LOGIN_TIME</li>" +
210                        "<li>OP_TYPE_GET_PASSWORD_RETIRED_TIME</li>" +
211                        "<li>OP_TYPE_GET_PW_CHANGED_BY_REQUIRED_TIME</li>" +
212                        "<li>OP_TYPE_GET_PW_CHANGED_TIME</li>" +
213                        "<li>OP_TYPE_GET_PW_EXPIRATION_WARNED_TIME</li>" +
214                        "<li>OP_TYPE_GET_PW_HISTORY</li>" +
215                        "<li>OP_TYPE_GET_PW_POLICY_DN</li>" +
216                        "<li>OP_TYPE_GET_PW_RESET_STATE</li>" +
217                        "<li>OP_TYPE_GET_REMAINING_AUTH_FAILURE_COUNT</li>" +
218                        "<li>OP_TYPE_GET_REMAINING_GRACE_LOGIN_COUNT</li>" +
219                        "<li>OP_TYPE_GET_RETIRED_PASSWORD_EXPIRATION_TIME</li>" +
220                        "<li>OP_TYPE_GET_SECONDS_UNTIL_ACCOUNT_ACTIVATION</li>" +
221                        "<li>OP_TYPE_GET_SECONDS_UNTIL_ACCOUNT_EXPIRATION</li>" +
222                        "<li>OP_TYPE_GET_SECONDS_UNTIL_AUTH_FAILURE_UNLOCK</li>" +
223                        "<li>OP_TYPE_GET_SECONDS_UNTIL_IDLE_LOCKOUT</li>" +
224                        "<li>OP_TYPE_GET_SECONDS_UNTIL_PW_EXPIRATION</li>" +
225                        "<li>OP_TYPE_GET_SECONDS_UNTIL_PW_EXPIRATION_WARNING</li>" +
226                        "<li>OP_TYPE_GET_SECONDS_UNTIL_PW_RESET_LOCKOUT</li>" +
227                        "<li>OP_TYPE_GET_SECONDS_UNTIL_REQUIRED_CHANGE_TIME</li>" +
228                        "<li>OP_TYPE_HAS_RETIRED_PASSWORD</li>" +
229                        "</ul>",
230                "The following attributes are available to use in a modify operation" +
231                        ":<br/>" +
232                        "<ul>" +
233                        "<li>" + ATTR_ACCOUNT_ACTIVATION_TIME + "</li>" +
234                        "<li>" + ATTR_ACCOUNT_DISABLED + "</li>" +
235                        "<li>" + ATTR_ACCOUNT_EXPIRATION_TIME + "</li>" +
236                        "<li>" + ATTR_ACCOUNT_EXPIRED + "</li>" +
237                        "<li>" + ATTR_ACCOUNT_FAILURE_LOCKED + "</li>" +
238                        "<li>" + ATTR_ACCOUNT_IDLE_LOCKED + "</li>" +
239                        "<li>" + ATTR_ACCOUNT_NOT_ACTIVE_YET + "</li>" +
240                        "<li>" + ATTR_ACCOUNT_RESET_LOCKED + "</li>" +
241                        "<li>" + ATTR_ACCOUNT_USABILITY_ERROR + "</li>" +
242                        "<li>" + ATTR_ACCOUNT_USABILITY_NOTICE + "</li>" +
243                        "<li>" + ATTR_ACCOUNT_USABILITY_WARNING + "</li>" +
244                        "<li>" + ATTR_ACCOUNT_USABLE + "</li>" +
245                        "<li>" + ATTR_AUTH_FAILURE_TIME + "</li>" +
246                        "<li>" + ATTR_AVAILABLE_SASL_MECHANISM + "</li>" +
247                        "<li>" + ATTR_AVAILABLE_TOTP_DELIVERY_MECHANISM + "</li>" +
248                        "<li>" + ATTR_FAILURE_LOCKOUT_TIME + "</li>" +
249                        "<li>" + ATTR_GRACE_LOGIN_USE_TIME + "</li>" +
250                        "<li>" + ATTR_IDLE_LOCKOUT_TIME + "</li>" +
251                        "<li>" + ATTR_LAST_LOGIN_IP_ADDRESS + "</li>" +
252                        "<li>" + ATTR_LAST_LOGIN_TIME + "</li>" +
253                        "<li>" + ATTR_PW_CHANGED_BY_REQUIRED_TIME + "</li>" +
254                        "<li>" + ATTR_PW_CHANGED_TIME + "</li>" +
255                        "<li>" + ATTR_PW_EXPIRATION_TIME + "</li>" +
256                        "<li>" + ATTR_PW_EXPIRATION_WARNED_TIME + "</li>" +
257                        "<li>" + ATTR_PW_EXPIRED + "</li>" +
258                        "<li>" + ATTR_PW_HISTORY + "</li>" +
259                        "<li>" + ATTR_PW_HISTORY_COUNT + "</li>" +
260                        "<li>" + ATTR_PW_RESET + "</li>" +
261                        "<li>" + ATTR_PW_RETIRED_TIME + "</li>" +
262                        "<li>" + ATTR_PWP_DN + "</li>" +
263                        "<li>" + ATTR_REMAINING_AUTH_FAILURE_COUNT + "</li>" +
264                        "<li>" + ATTR_REMAINING_GRACE_LOGIN_COUNT + "</li>" +
265                        //"<li>" + ATTR_RESET_LOCKOUT_TIME + "</li>" +
266                        "<li>" + ATTR_HAS_RETIRED_PASSWORD + "</li>" +
267                        "<li>" + ATTR_RETIRED_PASSWORD_EXPIRATION_TIME + "</li>" +
268                        "<li>" + ATTR_SECONDS_UNTIL_ACCOUNT_ACTIVATION + "</li>" +
269                        "<li>" + ATTR_SECONDS_UNTIL_ACCOUNT_EXPIRATION + "</li>" +
270                        "<li>" + ATTR_SECONDS_UNTIL_AUTH_FAILURE_UNLOCK + "</li>" +
271                        "<li>" + ATTR_SECONDS_UNTIL_IDLE_LOCKOUT + "</li>" +
272                        "<li>" + ATTR_SECONDS_UNTIL_PW_EXPIRATION + "</li>" +
273                        "<li>" + ATTR_SECONDS_UNTIL_PW_EXPIRATION_WARNING + "</li>" +
274                        "<li>" + ATTR_SECONDS_UNTIL_PW_RESET_LOCKOUT + "</li>" +
275                        "<li>" + ATTR_SECONDS_UNTIL_REQUIRED_CHANGED_TIME + "</li>" +
276                        "</ul>" +
277                        "<p>Example usage: <br/>" +
278                        "ldapmodify -D cn=directory\\ manager -w password<br/>" +
279                        "dn: uid=user.0,ou=people,dc=example,dc=com</br/>" +
280                        "changetype: modify<br/>" +
281                        "add: " + ATTR_ACCOUNT_DISABLED + "<br/>" +
282                        ATTR_ACCOUNT_DISABLED + ": true<br/>" +
283                        "<br/>" +
284                        "</p>"};
285    }
286  
287  /*
288bin/ldapmodify
289
290dn: uid=user.0,ou=people,o=data
291changetype: modify
292add: ds-pwp-user-state-set-account-disabled
293ds-pwp-user-state-set-account-disabled: false
294
295# Processing MODIFY request for uid=user.0,ou=people,o=data
296# MODIFY operation successful for DN uid=user.0,ou=people,o=data
297
298bin/ldapsearch -b uid=user.0,ou=people,o=data -s base "(&)getUserState
299
300dn: uid=user.0,ou=People,o=data
301ds-pwp-user-state-password-policy-dn: cn=Default Password Policy,cn=Password Policies,cn=config
302ds-pwp-user-state-account-usability-errors: code=1      name=account-disabled   message=The account has been disabled
303by an administrator
304ds-pwp-user-state-password-changed-time: 19700101000000.000Z
305ds-pwp-user-state-account-disabled-state: true
306ds-pwp-user-state-password-reset-state: false
307ds-pwp-user-state-remaining-grace-login-count: 0
308ds-pwp-user-state-has-retired-password: false
309
310bin/ldapmodify
311dn: uid=user.0,ou=people,o=data
312changetype: modify
313add: ds-pwp-user-state-clear-account-disabled
314ds-pwp-user-state-clear-account-disabled: true
315
316
317bin/ldapsearch -b uid=user.0,ou=people,o=data -s base "(&)getUserState
318
319dn: uid=user.0,ou=People,o=data
320ds-pwp-user-state-password-policy-dn: cn=Default Password Policy,cn=Password Policies,cn=config
321ds-pwp-user-state-password-changed-time: 19700101000000.000Z
322ds-pwp-user-state-account-disabled-state: false
323ds-pwp-user-state-password-reset-state: false
324ds-pwp-user-state-remaining-grace-login-count: 0
325ds-pwp-user-state-has-retired-password: false
326
327   */
328    
329    /**
330     * Updates the provided argument parser to include the arguments for this
331     * extension.
332     *
333     * @param parser The argument parser to update.
334     */
335    @Override
336    public void defineConfigArguments(final ArgumentParser parser)
337            throws ArgumentException
338    {
339        StringArgument dateFormatArgument = new StringArgument(ARG.NO_SHORTCUT, ARG_TIME_FORMAT, ARG.OPTIONAL, ARG
340                .UNIQUE, "{date-format}", "the" +
341                " " +
342                "date time format as " +
343                "per Java SimpleDateFormat specification. (Default: " + ARG_TIME_FORMAT_DEFAULT + ")",
344                ARG_TIME_FORMAT_DEFAULT);
345     /*
346      * ensure that the provided argument value is a valid pattern to avoid downstream issues
347      */
348        dateFormatArgument.addValueValidator(new ArgumentValueValidator()
349        {
350            @Override
351            public void validateArgumentValue(Argument argument, String valueString) throws ArgumentException
352            {
353                try
354                {
355                    new SimpleDateFormat(parser.getStringArgument(ARG_TIME_FORMAT).getValue());
356                } catch (IllegalArgumentException iae)
357                {
358                    throw new ArgumentException(StaticUtils.getExceptionMessage(iae));
359                }
360            }
361        });
362        parser.addArgument(dateFormatArgument);
363    }
364    
365    /**
366     * This method allows to validate configuration for the instance of the plugin is going to allow it to perform
367     * its task correctly.
368     *
369     * @param config              the plugin configuration
370     * @param parser              the argument parser for this extension
371     * @param unacceptableReasons a list of reasons for which the configuration was not acceptable
372     * @return Boolean the result of evaluating the parameters of the provided parser
373     */
374    @Override
375    public boolean isConfigurationAcceptable(final PluginConfig config,
376                                             final ArgumentParser parser,
377                                             List<String> unacceptableReasons)
378    {
379        return true;
380    }
381    
382    /**
383     * This method is called when a configuration change is made to the instance of the plugin.
384     * It is called by initializePlugin when the plugin is first instantiated and upon any configuration change
385     * thereafter
386     *
387     * @param config               the plugin configuration
388     * @param parser               the argument parser for this extension
389     * @param adminActionsRequired A list of messages of administrative actions required in order for configuration to
390     *                             be applied
391     * @param messages             A list of messages relating to applying the configuration for this extension
392     * @return an LDAP Result Code. ResultCode.SUCCESS if all arguments were satisfactory
393     */
394    @Override
395    public ResultCode applyConfiguration(final PluginConfig config,
396                                         final ArgumentParser parser,
397                                         List<String> adminActionsRequired,
398                                         List<String> messages)
399    {
400        try
401        {
402            dateFormatter = new SimpleDateFormat(parser.getStringArgument(ARG_TIME_FORMAT).getValue());
403            // disabling formatting leniency to avoid processing dates with inconsistent or unexpected formats
404            dateFormatter.setLenient(false);
405        } catch (IllegalArgumentException iae)
406        {
407            messages.add(StaticUtils.getExceptionMessage(iae));
408        }
409        return ResultCode.SUCCESS;
410    }
411    
412    /**
413     * Initializes this plugin.
414     * This method is called upon instantiation of the plugin and should
415     * ensure proper initialization.
416     *
417     * @param serverContext A handle to the server context for the server in
418     *                      which this extension is running.
419     * @param config        The general configuration for this plugin.
420     * @param parser        The argument parser which has been initialized from
421     *                      the configuration for this plugin.
422     * @throws LDAPException If a problem occurs while initializing this plugin.
423     */
424    @Override
425    public void initializePlugin(final DirectoryServerContext serverContext,
426                                 final PluginConfig config,
427                                 final ArgumentParser parser)
428            throws LDAPException
429    {
430        this.serverContext = serverContext;
431        
432        opsMap = new HashMap<>();
433        lcAddToMap(opsMap, OP_TYPE_GET_ACCOUNT_ACTIVATION_TIME, ATTR_ACCOUNT_ACTIVATION_TIME);
434        lcAddToMap(opsMap, OP_TYPE_GET_ACCOUNT_DISABLED_STATE, ATTR_ACCOUNT_DISABLED);
435        lcAddToMap(opsMap, OP_TYPE_GET_ACCOUNT_EXPIRATION_TIME, ATTR_ACCOUNT_EXPIRATION_TIME);
436        lcAddToMap(opsMap, OP_TYPE_GET_ACCOUNT_USABILITY_ERRORS, ATTR_ACCOUNT_USABILITY_ERROR);
437        lcAddToMap(opsMap, OP_TYPE_GET_ACCOUNT_USABILITY_NOTICES, ATTR_ACCOUNT_USABILITY_NOTICE);
438        lcAddToMap(opsMap, OP_TYPE_GET_AVAILABLE_OTP_DELIVERY_MECHANISMS, ATTR_AVAILABLE_TOTP_DELIVERY_MECHANISM);
439        lcAddToMap(opsMap, OP_TYPE_GET_ACCOUNT_USABILITY_WARNINGS, ATTR_ACCOUNT_USABILITY_WARNING);
440        lcAddToMap(opsMap, OP_TYPE_GET_ACCOUNT_IS_USABLE, ATTR_ACCOUNT_USABLE);
441        lcAddToMap(opsMap, OP_TYPE_GET_ACCOUNT_IS_NOT_YET_ACTIVE, ATTR_ACCOUNT_NOT_ACTIVE_YET);
442        lcAddToMap(opsMap, OP_TYPE_GET_ACCOUNT_IS_RESET_LOCKED, ATTR_ACCOUNT_RESET_LOCKED);
443        lcAddToMap(opsMap, OP_TYPE_GET_IDLE_LOCKOUT_TIME, ATTR_IDLE_LOCKOUT_TIME);
444        lcAddToMap(opsMap, OP_TYPE_GET_FAILURE_LOCKOUT_TIME, ATTR_FAILURE_LOCKOUT_TIME);
445        lcAddToMap(opsMap, OP_TYPE_GET_AUTH_FAILURE_TIMES, ATTR_AUTH_FAILURE_TIME);
446        lcAddToMap(opsMap, OP_TYPE_GET_AVAILABLE_SASL_MECHANISMS, ATTR_AVAILABLE_SASL_MECHANISM);
447        lcAddToMap(opsMap, OP_TYPE_GET_GRACE_LOGIN_USE_TIMES, ATTR_GRACE_LOGIN_USE_TIME);
448        lcAddToMap(opsMap, OP_TYPE_GET_LAST_LOGIN_IP_ADDRESS, ATTR_LAST_LOGIN_IP_ADDRESS);
449        lcAddToMap(opsMap, OP_TYPE_GET_LAST_LOGIN_TIME, ATTR_LAST_LOGIN_TIME);
450        lcAddToMap(opsMap, OP_TYPE_GET_PASSWORD_RETIRED_TIME, ATTR_PW_RETIRED_TIME);
451        lcAddToMap(opsMap, OP_TYPE_GET_PW_CHANGED_BY_REQUIRED_TIME, ATTR_PW_CHANGED_BY_REQUIRED_TIME);
452        lcAddToMap(opsMap, OP_TYPE_GET_PW_CHANGED_TIME, ATTR_PW_CHANGED_TIME);
453        lcAddToMap(opsMap, OP_TYPE_GET_PW_EXPIRATION_WARNED_TIME, ATTR_PW_EXPIRATION_WARNED_TIME);
454        lcAddToMap(opsMap, OP_TYPE_GET_PW_HISTORY, ATTR_PW_HISTORY);
455        lcAddToMap(opsMap, OP_TYPE_GET_PW_POLICY_DN, ATTR_PWP_DN);
456        lcAddToMap(opsMap, OP_TYPE_GET_PW_RESET_STATE, ATTR_PW_RESET);
457        lcAddToMap(opsMap, OP_TYPE_GET_REMAINING_AUTH_FAILURE_COUNT, ATTR_REMAINING_AUTH_FAILURE_COUNT);
458        lcAddToMap(opsMap, OP_TYPE_GET_REMAINING_GRACE_LOGIN_COUNT, ATTR_REMAINING_GRACE_LOGIN_COUNT);
459        lcAddToMap(opsMap, OP_TYPE_GET_RETIRED_PASSWORD_EXPIRATION_TIME, ATTR_RETIRED_PASSWORD_EXPIRATION_TIME);
460        lcAddToMap(opsMap, OP_TYPE_GET_SECONDS_UNTIL_ACCOUNT_ACTIVATION, ATTR_SECONDS_UNTIL_ACCOUNT_ACTIVATION);
461        lcAddToMap(opsMap, OP_TYPE_GET_SECONDS_UNTIL_ACCOUNT_EXPIRATION, ATTR_SECONDS_UNTIL_ACCOUNT_EXPIRATION);
462        lcAddToMap(opsMap, OP_TYPE_GET_SECONDS_UNTIL_AUTH_FAILURE_UNLOCK, ATTR_SECONDS_UNTIL_AUTH_FAILURE_UNLOCK);
463        lcAddToMap(opsMap, OP_TYPE_GET_SECONDS_UNTIL_IDLE_LOCKOUT, ATTR_SECONDS_UNTIL_IDLE_LOCKOUT);
464        lcAddToMap(opsMap, OP_TYPE_GET_SECONDS_UNTIL_PW_EXPIRATION, ATTR_SECONDS_UNTIL_PW_EXPIRATION);
465        lcAddToMap(opsMap, OP_TYPE_GET_SECONDS_UNTIL_PW_EXPIRATION_WARNING, ATTR_SECONDS_UNTIL_PW_EXPIRATION_WARNING);
466        lcAddToMap(opsMap, OP_TYPE_GET_SECONDS_UNTIL_PW_RESET_LOCKOUT, ATTR_SECONDS_UNTIL_PW_RESET_LOCKOUT);
467        lcAddToMap(opsMap, OP_TYPE_GET_SECONDS_UNTIL_REQUIRED_CHANGE_TIME, ATTR_SECONDS_UNTIL_REQUIRED_CHANGED_TIME);
468        lcAddToMap(opsMap, OP_TYPE_HAS_RETIRED_PASSWORD, ATTR_HAS_RETIRED_PASSWORD);
469        lcAddToMap(opsMap, OP_TYPE_GET_PW_EXPIRATION_TIME, ATTR_PW_EXPIRATION_TIME);
470        lcAddToMap(opsMap, OP_TYPE_GET_PW_IS_EXPIRED, ATTR_PW_EXPIRED);
471        lcAddToMap(opsMap, OP_TYPE_GET_PW_HISTORY_COUNT, ATTR_PW_HISTORY_COUNT);
472//        lcAddToMap(opsMap,OP_TYPE_GET_RESET_LOCKOUT_TIME, ATTR_RESET_LOCKOUT_TIME);
473        lcAddToMap(opsMap, OP_TYPE_GET_ACCOUNT_IS_EXPIRED, ATTR_ACCOUNT_EXPIRED);
474        lcAddToMap(opsMap, OP_TYPE_GET_ACCOUNT_IS_FAILURE_LOCKED, ATTR_ACCOUNT_FAILURE_LOCKED);
475        lcAddToMap(opsMap, OP_TYPE_GET_ACCOUNT_IS_IDLE_LOCKED, ATTR_ACCOUNT_IDLE_LOCKED);
476        
477        
478        // Use the applyConfiguration method to set up the plugin.
479        final ArrayList<String> adminActionsRequired = new ArrayList<>(5);
480        final ArrayList<String> messages = new ArrayList<>(5);
481        final ResultCode resultCode = applyConfiguration(config, parser, adminActionsRequired, messages);
482        if (resultCode != ResultCode.SUCCESS)
483        {
484            throw new LDAPException(resultCode,
485                    "One or more errors occurred while trying to initialize  " + getExtensionName() + " Plugin  in '"
486                            + config.getConfigObjectDN() + ":  " +
487                            StaticUtils.concatenateStrings(messages));
488        }
489    }
490    
491    private void lcAddToMap(Map<Integer, String> map, Integer i, String s)
492    {
493        map.put(i, s.toLowerCase());
494    }
495    
496    /**
497     * Performs the necessary processing to add password policy state to entries resulting of an LDAP search request
498     * that includes a trigger attribute as per the configuration of this instance of the plugin.
499     * <p>
500     * If the trigger attribute is included, the extended operation is issued internally and each state return is
501     * added to
502     * the result entry as virtual attributes.
503     *
504     * @param operationContext the operation context
505     * @param request          the search request
506     * @param entry            the search result entry
507     * @param controls         a list of controls provided with the search request
508     * @param result           The result that will be returned to the client if
509     *                         the plugin result indicates that processing on
510     *                         the operation should be interrupted.  It may be
511     *                         altered if desired.
512     * @return Information about the result of the plugin processing.
513     */
514    @Override
515    public SearchEntryPluginResult doSearchEntry(final ActiveSearchOperationContext operationContext,
516                                                 final SearchRequest request,
517                                                 UpdatableSearchResult result,
518                                                 UpdatableEntry entry,
519                                                 List<Control> controls)
520    {
521        SearchEntryPluginResult pluginResult = SearchEntryPluginResult.SUCCESS;
522        // nothing to do if the request is null, moving on
523        if (request == null || request.getAttributes() == null || request.getAttributes().isEmpty())
524        {
525            return pluginResult;
526        }
527        
528        // verify that the request matches the criteria, i.e. that it includes the configured trigger attribute
529        List<String> requestedAttributes = request.getAttributes();
530    
531        if (!searchRequestMatch(requestedAttributes))
532        {
533            return pluginResult;
534        }
535    
536        boolean hallPass = false;
537        List<String> pwpUserStateAttributes = new ArrayList<>();
538        for (int i = 0; i < requestedAttributes.size(); i++)
539        {
540            if (requestedAttributes.get(i).equalsIgnoreCase(ATTR_GET_ALL))
541            {
542                hallPass = true;
543                break;
544            } else
545            {
546                pwpUserStateAttributes.add(requestedAttributes.get(i).toLowerCase());
547            }
548        }
549        
550        try
551        {
552            // if all criteria have been met, issue the extended operation for the search result entry
553            ExtendedResult extResult = operationContext.getInternalUserConnection().processExtendedOperation(new
554                    PasswordPolicyStateExtendedRequest(entry.getDN()));
555            PasswordPolicyStateExtendedResult pwpResult = new PasswordPolicyStateExtendedResult(extResult);
556            if (pwpResult.getResultCode() != ResultCode.SUCCESS)
557            {
558                result.setResultCode(pwpResult.getResultCode());
559                result.setDiagnosticMessage(pwpResult.getDiagnosticMessage());
560                return new SearchEntryPluginResult(false, false, false, false);
561            }
562      
563            /*
564            add each available password policy state operation result as attribute to the result entry before sending it
565            back to the client. Each attribute is preceded with a prefix as configured
566            */
567            
568            for (PasswordPolicyStateOperation pwpOp : pwpResult.getOperations())
569            {
570                if (pwpOp == null) continue;
571                // this is always going to be lower case, we ensure that when populating the map
572                String attributeName = opsMap.get(pwpOp.getOperationType());
573                // because the same precaution is taken when building the requestedAttribute list, case doesn't matter
574                // this is to ensure that if an attribute is explicitly provided as opposed to get-all
575                // we only return the requested attributes
576                // it might important for certain clients
577                if (hallPass || pwpUserStateAttributes.contains(attributeName))
578                {
579                    Attribute attribute = makeAttr(attributeName, pwpOp.getStringValues());
580                    if (attribute == null) continue;
581                    entry.addAttribute(attribute);
582                }
583            }
584        } catch (LDAPException e)
585        {
586            serverContext.debugCaught(e);
587            serverContext.logMessage(LogSeverity.MILD_ERROR, e.getDiagnosticMessage());
588        }
589        
590        return pluginResult;
591    }
592    
593    private Attribute makeAttr(String name, String... values)
594    {
595        // the attribute name is null?
596        if (name == null || name.isEmpty()) return null;
597        
598        // no values provided ?
599        if (values == null) return null;
600        if (values.length == 0) return null;
601        // single null or empty value provided ?
602        if (values.length == 1 && (values[0] == null || values[0].isEmpty())) return null;
603        
604        // return an attribute
605        return new Attribute(name, values);
606    }
607    
608    /**
609     * Performs processing before the server attempts to parse the LDAP Modify request.
610     * If the request includes attributes that match what this instance of the plugin
611     * is configured to convert to a PasswordPolicyStateExtendedRequest then the
612     * attribute and possibly the attribute value will be parsed and stripped from the
613     * modify request.
614     * <p>
615     * If the instance of the plugin is configured to authenticate the request with TOTP then it fail altogether if
616     * no TOTP code is provided or if it is invalid.
617     * <p>
618     * If the PasswordPolicyStateExtendedRequest is successful and there are remaining modifications, the remaining
619     * modifications will be passed on for normal processing.
620     *
621     * @param operationContext the operation context
622     * @param request          the modify request
623     * @param result           The result that will be returned to the client if
624     *                         the plugin result indicates that processing on
625     *                         the operation should be interrupted.  It may be
626     *                         altered if desired.
627     * @return Information about the result of the plugin processing.
628     */
629    public PreParsePluginResult doPreParse(final ActiveOperationContext operationContext,
630                                           UpdatableModifyRequest request,
631                                           UpdatableModifyResult result)
632    {
633        PreParsePluginResult pluginResult = PreParsePluginResult.SUCCESS;
634        // nothing to do if the request is null, moving on
635        if (request == null)
636        {
637            return pluginResult;
638        }
639        
640        // Check if any of the modifications included with the request match
641        if (!modifyRequestMatch(request))
642        {
643            return pluginResult;
644        }
645
646
647    /*
648     * duplicate the modifications from the LDAP Modify request
649     */
650        CopyOnWriteArrayList<Modification> mods = new CopyOnWriteArrayList<>();
651        for (Modification mod : request.getModifications())
652        {
653            if (mod == null)
654            {
655                continue;
656            }
657            mods.add(mod);
658        }
659        
660        // nothing to do if there are no modifications, moving on
661        if (mods.isEmpty())
662        {
663            return pluginResult;
664        }
665    
666
667        /*
668          Build a list of operations to include in the PasswordPolicyStateExtendedRequest
669         */
670        List<PasswordPolicyStateOperation> ops = new ArrayList<>();
671        for (Modification mod : request.getModifications())
672        {
673            if (mod == null)
674            {
675                continue;
676            }
677            
678            Attribute modificationAttribute = mod.getAttribute();
679            
680            String attributeName = modificationAttribute.getName();
681            if (attributeName == null || attributeName.isEmpty())
682            {
683                mods.remove(mod);
684                continue;
685            }
686            
687            String attributeValue = modificationAttribute.getValue();
688    
689            // Ignore add modifications with empty values
690            if (mod.getModificationType().equals(ModificationType.ADD) && (attributeValue == null || attributeValue
691                    .isEmpty()))
692            {
693                mods.remove(mod);
694                continue;
695            }
696            
697            /*
698            if (ATTR_PW_EXPIRED.equalsIgnoreCase(attributeName))
699            {
700                mods.remove(mod);
701                if (isDelete(mod))
702                {
703                    ops.add(PasswordPolicyStateOperation.createClearAccountExpirationTimeOperation());
704                } else
705                {
706                    String[] values = mod.getValues();
707                    if (values.length > 1)
708                    {
709                        result.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
710                        String message = "modification for " + ATTR_PW_EXPIRED
711                                + " must have a single boolean value. Ignoring.";
712                        result.setDiagnosticMessage(message);
713                        serverContext.logMessage(LogSeverity.MILD_ERROR, message);
714                        continue;
715                    }
716                    ops.add(PasswordPolicyStateOperation.createSetAccountExpirationTimeOperation((values[0])));
717                }
718            }
719            */
720            
721            /*
722              manage the current time to the set of times that the user has unsuccessfully tried to authenticate since
723              the last successful authentication
724            */
725            if (ATTR_AUTH_FAILURE_TIME.equalsIgnoreCase(attributeName))
726            {
727                mods.remove(mod);
728                if (isDelete(mod))
729                {
730                    ops.add(PasswordPolicyStateOperation.createClearAuthenticationFailureTimesOperation());
731                } else
732                {
733                    // when here, the modification is neither a delete nor a replace with an empty value
734                    String[] values = mod.getValues();
735                    Date[] dates = new Date[values.length];
736                    int i = 0;
737                    for (String value : values)
738                    {
739                        if (isNow(value))
740                        {
741                            dates[i++] = new Date();
742                        } else
743                        {
744                            try
745                            {
746                                Date d = dateFormatter.parse(value);
747                                dates[i++] = d;
748                            } catch (ParseException e)
749                            {
750                                result.setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
751                                result.setDiagnosticMessage("The syntax for " + ATTR_AUTH_FAILURE_TIME + " " +
752                                        "was not valid. The expected format is " + dateFormatter.toPattern());
753                                return new PreParsePluginResult(false, false, true, true);
754                            }
755                        }
756                        if (mod.getModificationType() == ModificationType.REPLACE)
757                        {
758                            ops.add(PasswordPolicyStateOperation.createSetAuthenticationFailureTimesOperation
759                                    (dates));
760                        } else
761                        {
762                            ops.add(PasswordPolicyStateOperation.createAddAuthenticationFailureTimeOperation());
763                        }
764                    }
765                }
766                continue;
767            }
768    
769            if (ATTR_ACCOUNT_FAILURE_LOCKED.equalsIgnoreCase(attributeName))
770            {
771                mods.remove(mod);
772                if (isDelete(mod))
773                {
774                    ops.add(PasswordPolicyStateOperation.createSetAccountIsFailureLockedOperation(false));
775                } else
776                {
777                    String[] values = mod.getValues();
778                    if (values.length > 1)
779                    {
780                        result.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
781                        String message = "modification for " + ATTR_ACCOUNT_FAILURE_LOCKED
782                                + " must have a single boolean value. Ignoring.";
783                        result.setDiagnosticMessage(message);
784                        serverContext.logMessage(LogSeverity.MILD_ERROR, message);
785                        continue;
786                    }
787                    ops.add(PasswordPolicyStateOperation.createSetAccountIsFailureLockedOperation
788                            (parseBooleanPermissive(values[0])));
789                }
790                continue;
791            }
792
793            /*
794            add the current time to the set of times that the user has authenticated using grace logins since his/her
795            password expired
796            */
797            if (ATTR_GRACE_LOGIN_USE_TIME.equalsIgnoreCase(attributeName))
798            {
799                if (isDelete(mod))
800                {
801                    ops.add(PasswordPolicyStateOperation.createClearGraceLoginUseTimesOperation());
802                } else
803                {
804                    String[] values = mod.getValues();
805                    Date[] dates = new Date[values.length];
806                    int i = 0;
807                    for (String value : values)
808                    {
809                        if (isNow(value))
810                        {
811                            dates[i++] = new Date();
812                        } else
813                        {
814                            try
815                            {
816                                Date d = dateFormatter.parse(value);
817                                dates[i++] = d;
818                            } catch (ParseException e)
819                            {
820                                result.setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
821                                result.setDiagnosticMessage("The syntax for " + ATTR_GRACE_LOGIN_USE_TIME + " " +
822                                        "was not valid. The expected format is " + dateFormatter.toPattern());
823                                return new PreParsePluginResult(false, false, true, true);
824                            }
825                        }
826                        if (mod.getModificationType() == ModificationType.REPLACE)
827                        {
828                            ops.add(PasswordPolicyStateOperation.createSetGraceLoginUseTimesOperation(dates));
829                        } else
830                        {
831                            ops.add(PasswordPolicyStateOperation.createAddGraceLoginUseTimeOperation());
832                        }
833                    }
834                }
835                mods.remove(mod);
836                continue;
837            }
838
839            /*
840            clear the account expiration time in the user's entry
841            */
842            if (ATTR_ACCOUNT_ACTIVATION_TIME.equalsIgnoreCase(attributeName))
843            {
844                if (isDelete(mod))
845                {
846                    ops.add(PasswordPolicyStateOperation.createClearAccountActivationTimeOperation());
847                } else
848                {
849                    String[] values = mod.getValues();
850                    Date d;
851                    if (isNow(values[0]))
852                    {
853                        d = new Date();
854                    } else
855                    {
856                        try
857                        {
858                            d = dateFormatter.parse(values[0]);
859                        } catch (ParseException e)
860                        {
861                            result.setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
862                            result.setDiagnosticMessage("The syntax for " + ATTR_ACCOUNT_ACTIVATION_TIME + " " +
863                                    "was not valid. The expected format is " + dateFormatter.toPattern());
864                            return new PreParsePluginResult(false, false, true, true);
865                        }
866                    }
867                    ops.add(PasswordPolicyStateOperation.createSetAccountActivationTimeOperation(d));
868                }
869                
870                mods.remove(mod);
871                continue;
872            }
873            
874            /*
875            handle user account failure lockout time in the user entry
876             */
877            if (ATTR_FAILURE_LOCKOUT_TIME.equalsIgnoreCase(attributeName))
878            {
879                mods.remove(mod);
880                if (isDelete(mod))
881                {
882                    ops.add(PasswordPolicyStateOperation.createSetAccountIsFailureLockedOperation(false));
883                } else
884                {
885                    // TODO: allow to use replace with a boolean
886                }
887                continue;
888            }
889            
890            /*
891            handle user account disabled state in the user's entry
892            */
893            if (ATTR_ACCOUNT_DISABLED.equalsIgnoreCase(attributeName))
894            {
895                mods.remove(mod);
896                String[] values = mod.getValues();
897                if (isDelete(mod))
898                {
899                    ops.add(PasswordPolicyStateOperation.createClearAccountDisabledStateOperation());
900                } else
901                {
902                    if (values.length != 1)
903                    {
904                        result.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
905                        String message = "modification for " + ATTR_ACCOUNT_DISABLED
906                                + " must have a single boolean value. Ignoring.";
907                        result.setDiagnosticMessage(message);
908                        serverContext.logMessage(LogSeverity.MILD_ERROR, message);
909                        continue;
910                    }
911    
912                    ops.add(PasswordPolicyStateOperation.createSetAccountDisabledStateOperation
913                            (parseBooleanPermissive(values[0])));
914    
915                }
916                continue;
917            }
918
919            /*
920            handle the account expiration time in the user's entry
921            */
922            if (ATTR_ACCOUNT_EXPIRATION_TIME.equalsIgnoreCase(attributeName))
923            {
924                mods.remove(mod);
925                if (isDelete(mod))
926                {
927                    ops.add(PasswordPolicyStateOperation.createClearAccountExpirationTimeOperation());
928                } else
929                {
930                    // TODO: this seems to be incomplete to function properly? We should parse the values to dates
931                    ops.add(new PasswordPolicyStateOperation(OP_TYPE_SET_ACCOUNT_EXPIRATION_TIME, mod.getRawValues()));
932                }
933                continue;
934            }
935            
936
937            /*
938            clear the last login IP address from the user's entry
939            */
940            if (ATTR_LAST_LOGIN_IP_ADDRESS.equalsIgnoreCase(attributeName))
941            {
942                mods.remove(mod);
943                if (isDelete(mod))
944                {
945                    ops.add(PasswordPolicyStateOperation.createClearLastLoginIPAddressOperation());
946                } else
947                {
948                    // TODO: throw an exception if multiple values are provided
949                    ops.add(PasswordPolicyStateOperation.createSetLastLoginIPAddressOperation(mod.getValues()[0]));
950                }
951                continue;
952            }
953
954            /*
955            clear the last login time from the user's entry
956            */
957            if (ATTR_LAST_LOGIN_TIME.equalsIgnoreCase(attributeName))
958            {
959                mods.remove(mod);
960                if (isDelete(mod))
961                {
962                    ops.add(PasswordPolicyStateOperation.createClearLastLoginTimeOperation());
963                } else
964                {
965                    //TODO: throw an exception if multiple values are provided
966                    Date d = null;
967                    if (isNow(mod.getValues()[0]))
968                    {
969                        d = new Date();
970                    } else
971                    {
972                        try
973                        {
974                            d = dateFormatter.parse(mod.getValues()[0]);
975                        } catch (ParseException e)
976                        {
977                            e.printStackTrace();
978                        }
979                    }
980                    ops.add(PasswordPolicyStateOperation.createSetLastLoginTimeOperation(d));
981                }
982                continue;
983            }
984
985            /*
986            clear the password changed time in the user's account
987            */
988            if (ATTR_PW_CHANGED_TIME.equalsIgnoreCase(attributeName))
989            {
990                mods.remove(mod);
991                if (isDelete(mod))
992                {
993                    ops.add(new PasswordPolicyStateOperation(OP_TYPE_CLEAR_PW_CHANGED_TIME));
994                } else
995                {
996                    ops.add(new PasswordPolicyStateOperation(OP_TYPE_SET_PW_CHANGED_TIME, mod.getRawValues()));
997                }
998                continue;
999            }
1000
1001      /*
1002      clear the last required password change time from the user's entry
1003       */
1004      /*
1005            if (attributeMatches(attributeName, ))
1006            {
1007                ops.add(PasswordPolicyStateOperation.createClearPasswordChangedByRequiredTimeOperation());
1008                mods.remove(mod);
1009                continue;
1010            }
1011            */
1012
1013      /*
1014      clear the password expiration warned time from the user's entry
1015       */
1016      /*
1017            if (attributeMatches(attributeName, ATTR_CLEAR_PW_EXPIRATION_WARNED_TIME))
1018            {
1019                ops.add(PasswordPolicyStateOperation.createClearPasswordExpirationWarnedTimeOperation());
1020                mods.remove(mod);
1021                continue;
1022            }
1023            */
1024
1025            /*
1026            clear the password history values stored in the user's entry
1027            */
1028            if (ATTR_PW_HISTORY.equalsIgnoreCase(attributeName))
1029            {
1030                mods.remove(mod);
1031                if (isDelete(mod))
1032                {
1033                    ops.add(PasswordPolicyStateOperation.createClearPasswordHistoryOperation());
1034                } else
1035                {
1036                    serverContext.logMessage(LogSeverity.MILD_WARNING, "unsupported operation type on " +
1037                            ATTR_PW_HISTORY);
1038                }
1039                continue;
1040            }
1041
1042            /*
1043            clear the password reset state information in the user's entry
1044            */
1045            if (ATTR_PW_RESET.equalsIgnoreCase(attributeName))
1046            {
1047                mods.remove(mod);
1048                if (isDelete(mod))
1049                {
1050                    ops.add(PasswordPolicyStateOperation.createClearPasswordResetStateOperation());
1051                } else
1052                {
1053                    ops.add(PasswordPolicyStateOperation.createSetPasswordResetStateOperation(parseBooleanPermissive
1054                            (mod.getValues()[0])));
1055                }
1056                continue;
1057            }
1058
1059            /*
1060            purge any retired password from the user's entry
1061            */
1062            if (ATTR_HAS_RETIRED_PASSWORD.equalsIgnoreCase(attributeName))
1063            {
1064                mods.remove(mod);
1065                if (isDelete(mod))
1066                {
1067                    ops.add(PasswordPolicyStateOperation.createPurgeRetiredPasswordOperation());
1068                } else
1069                {
1070                    serverContext.logMessage(LogSeverity.MILD_WARNING, "unsupported operation type for " +
1071                            ATTR_HAS_RETIRED_PASSWORD);
1072                }
1073            }
1074        }
1075        
1076        if (!ops.isEmpty())
1077        {
1078            try
1079            {
1080    
1081                PasswordPolicyStateOperation[] operations = new PasswordPolicyStateOperation[ops.size()];
1082                operations = ops.toArray(operations);
1083                PasswordPolicyStateExtendedRequest passwordPolicyStateExtendedRequest = new
1084                        PasswordPolicyStateExtendedRequest(request.getDN(), operations);
1085                ExtendedResult extResult = operationContext.getInternalUserConnection().processExtendedOperation
1086                        (passwordPolicyStateExtendedRequest);
1087                PasswordPolicyStateExtendedResult pwpResult = new PasswordPolicyStateExtendedResult(extResult);
1088                if (pwpResult.getResultCode() != ResultCode.SUCCESS)
1089                {
1090                        
1091                        /*
1092                        if there is an error during the processing of the extended operation we interrupt the flow
1093                        to return the error to the client right away
1094                         */
1095                    result.setResultCode(pwpResult.getResultCode());
1096                    result.setDiagnosticMessage(pwpResult.getDiagnosticMessage());
1097                    result.setAdditionalLogMessage("User state update error");
1098                    pluginResult = new PreParsePluginResult(false, false, true, true);
1099                }
1100            } catch (LDAPException e)
1101            {
1102                operationContext.getServerContext().debugCaught(e);
1103            }
1104        }
1105        
1106        if (mods.isEmpty())
1107        {
1108                /*
1109                  if mods is empty, there are no remaining modifications to process for the core, we can return the
1110                  result of the extended operation as is right away
1111                 */
1112            result.setResultCode(ResultCode.SUCCESS);
1113            pluginResult = new PreParsePluginResult(false, false, true, true);
1114        } else
1115        {
1116                /*
1117                if on the other hand there are some modifications that the core should process then we update then
1118                we update the original incoming request with that and let it do its job
1119                 */
1120            request.setModifications(mods);
1121        }
1122        return pluginResult;
1123    }
1124    
1125    @Override
1126    public PreParsePluginResult doPreParse(ActiveOperationContext operationContext, UpdatableAddRequest request,
1127                                           UpdatableAddResult result)
1128    {
1129        
1130        List<PasswordPolicyStateOperation> ops = new ArrayList<>();
1131        // strip out the attribute from the incoming entry
1132        List<Attribute> attribute = request.getEntry().getAttribute(ATTR_PW_RESET);
1133        if (attribute != null)
1134        {
1135            ops.add(PasswordPolicyStateOperation.createSetPasswordResetStateOperation(parseBooleanPermissive
1136                    (attribute.get(0).getValue())));
1137            request.getEntry().removeAttribute(ATTR_PW_RESET);
1138        }
1139        operationContext.setAttachment(ATTR_EXTENSION, ops);
1140        
1141        return PreParsePluginResult.SUCCESS;
1142    }
1143    
1144    @Override
1145    public PostOperationPluginResult doPostOperation(ActiveOperationContext operationContext, AddRequest request,
1146                                                     UpdatableAddResult result)
1147    {
1148        if (ResultCode.SUCCESS.equals(result.getResultCode()))
1149        {
1150            List<PasswordPolicyStateOperation> ops = (List<PasswordPolicyStateOperation>) operationContext
1151                    .getAttachment(ATTR_EXTENSION);
1152            if (ops != null && ops.size() > 0)
1153            {
1154                PasswordPolicyStateOperation[] operations = new PasswordPolicyStateOperation[ops.size()];
1155                operations = ops.toArray(operations);
1156                PasswordPolicyStateExtendedRequest passwordPolicyStateExtendedRequest = new
1157                        PasswordPolicyStateExtendedRequest(request.getEntry().getDN(), operations);
1158                ExtendedResult extResult = null;
1159                try
1160                {
1161                    extResult = operationContext.getInternalUserConnection().processExtendedOperation
1162                            (passwordPolicyStateExtendedRequest);
1163                    PasswordPolicyStateExtendedResult pwpResult = new PasswordPolicyStateExtendedResult(extResult);
1164                    if (pwpResult.getResultCode() != ResultCode.SUCCESS)
1165                    {
1166                        result.setAdditionalLogMessage(pwpResult.getExtendedResultName());
1167                    }
1168                } catch (LDAPException e)
1169                {
1170                    result.setResultCode(extResult.getResultCode());
1171                    result.setAdditionalLogMessage("The initial add of entry " + request.getEntry().getDN() + " " +
1172                            "succeeded but the subsequent user state operation failed with the following error: " +
1173                            extResult.getExtendedResultName() + " " + extResult.getDiagnosticMessage());
1174                    return new PostOperationPluginResult(false, false);
1175                }
1176            }
1177        }
1178        return PostOperationPluginResult.SUCCESS;
1179    }
1180    
1181    /**
1182     * Performs all necessary processing to check if a search request matches the criteria for execution of this
1183     * instance of the plugin
1184     *
1185     * @param attributes a list of attribute names to evaluate
1186     * @return true if the search request satisfies the criteria for this instance of the plugin to attempt
1187     * processing
1188     */
1189
1190    private boolean searchRequestMatch(List<String> attributes)
1191    {
1192        if (attributes != null)
1193        {
1194            String prefix = ATTR_PREFIX.toLowerCase();
1195    
1196            for (String s : attributes)
1197            {
1198                if (s == null || s.isEmpty()) continue;
1199    
1200                if (s.equalsIgnoreCase(ATTR_GET_ALL) || s.startsWith(prefix))
1201                {
1202                    return true;
1203                }
1204            }
1205        }
1206        return false;
1207    }
1208    
1209    /**
1210     * Performs all processing to check if a modify request matches the criteria for execution of this instance of the
1211     * plugin
1212     *
1213     * @param modifyRequest to modify request to evaluate
1214     * @return true if the modify request satisfies the criteria for this instance of the plugin to
1215     * attempt processing
1216     */
1217    private boolean modifyRequestMatch(final UpdatableModifyRequest modifyRequest)
1218    {
1219        if (modifyRequest != null)
1220        {
1221            for (Modification mod : modifyRequest.getModifications())
1222            {
1223                if (mod == null) continue;
1224                String name = mod.getAttributeName();
1225                if (name == null || name.isEmpty()) continue;
1226                
1227                if (name.equalsIgnoreCase(ATTR_GET_ALL) || name.toLowerCase().startsWith(ATTR_PREFIX.toLowerCase()))
1228                {
1229                    return true;
1230                }
1231            }
1232        }
1233        return false;
1234    }
1235    
1236    private boolean parseBooleanPermissive(String value)
1237    {
1238        if (value == null | value.isEmpty())
1239        {
1240            return false;
1241        }
1242    
1243        String v = value.trim();
1244    
1245        boolean result =
1246                "yes".equalsIgnoreCase(v)
1247                        || "y".equalsIgnoreCase(v)
1248                        || "true".equalsIgnoreCase(v)
1249                        || "1".equalsIgnoreCase(v);
1250        return result;
1251    
1252    }
1253    
1254    private boolean isNow(String value)
1255    {
1256        return value == null || value.isEmpty() || NOW.equalsIgnoreCase(value);
1257    }
1258    
1259    private boolean isDelete(Modification mod)
1260    {
1261        // That's the obvious case ... it is explicitly a delete
1262        if (mod.getModificationType().equals(ModificationType.DELETE))
1263        {
1264            return true;
1265        }
1266        
1267        // Less obvious, a replace with no value is a delete
1268        if (mod.getModificationType().equals(ModificationType.REPLACE))
1269        {
1270            String[] values = mod.getValues();
1271            if (values == null || values.length == 0)
1272            {
1273                return true;
1274            }
1275    
1276            return values[0] == null || values[0].isEmpty();
1277        }
1278        return false;
1279    }
1280    
1281    static class ARG
1282    {
1283        static final Character NO_SHORTCUT = null;
1284        static final Integer UNIQUE = 1;
1285        static final Boolean OPTIONAL = Boolean.FALSE;
1286    }
1287}