# SecurityCache.java

    /*
    ** Module   : SecurityCache.java
    ** Abstract : reads P2J directory and creates a cache of accounts, ACLs etc
    **
    ** Copyright (c) 2005-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ----------------------------------Description----------------------------------
    ** 001 NVS 20050307   @20208 Created. This class reads P2J directory and
    **                           creates a cache of accounts, ACLs etc.
    ** 002 SIY 20050329   @20558 Minor cleanups in code and comments.
    ** 003 NVS 20050414   @20711 Fixed bug in readACLs() showed only in multi-
    **                           plugin configuration. Changed printACLs()
    **                           output for rights.
    ** 004 NVS 20050415   @20735 Fixed a bug in mergeACLs() method. The bug
    **                           was caused by using single instance of
    **                           bindings list for multiple instances of the
    **                           resource. Each resource instance has to have
    **                           its private copy of the bindings list or else
    **                           the merge results would be visible for other
    **                           resource instances, which is wrong.
    **                           The instance of binding object (single item
    **                           in the binding list before merge) still can
    **                           be shared between multiple resource instances
    **                           as currently there is no attempt to merge
    **                           multiple binding objects in the list into a
    **                           combined object (the latter is theoretically
    **                           possible if there is a way to tell two rights
    **                           objects are equal).
    ** 005 NVS 20050425   @20894 Directory plugin needs special init sequence
    **                           because everything is read from directory
    **                           before contexts and ACLs are created.
    **                           added securityIsUp() call at the end of
    **                           security cache creation.
    ** 006 NVS 20050502   @20984 binding directory object has changed. It's no
    **                           longer multi-valued. Multiple sortable
    **                           objects are used instead.
    ** 007 NVS 20050505   @21109 Added serial number private field and method
    **                           to get it. This is required for security
    **                           cache refresh bug fixing. Changed the call to
    **                           SecurityContext constructor for initial
    **                           security context as now it stores the
    **                           reference to this cache.
    ** 008 NVS 20050525   @21272 Resource plugins now provide scoped variables
    **                           and their libraries are registered in single
    **                           instance of SymbolResolver which has become
    **                           the property of the security cache.
    ** 009 NVS 20050620   @21528 ACLs are divided into shared and server-
    **                           specific ones. Shared ALCs are read from
    **                           /security/acl node. Server-specific ones are
    **                           read from /security/acls/serverId where the
    **                           serverId portion matches the server ID.
    ** 010 NVS 20050727   @21867 Initial security context of security cache is
    **                           now permanently bound to DirectoryService
    **                           upon creation. bind()/unbind() calls removed
    **                           from SecurityCache constructor.
    ** 011 NVS 20050912   @22674 Added constant definition for authentication
    **                           result AUTH_RESULT_FAILURE_RETRIABLE and a
    **                           method getAuthRetries() to get the configured
    **                           number of retries.
    ** 012 NVS 20060330   @25284 Added getAllAccountIds() method.
    ** 013 NVS 20060417   @25562 Added password aging support: reading aging
    **                           parameters from /security/config/change and
    **                           storing password change date/time in the user
    **                           accounts. Added new methods: getMaxAge() and
    **                           getPasswordInput().
    ** 014 NVS 20060418   @25583 Password input hook instantiation had to be
    **                           delayed till the use time.
    ** 015 NVS 20060426   @25742 Fixed a regression in the mergeAcls() method
    **                           causing ACLs not to be merged properly.
    ** 016 GES 20061003   @30109 Moved constants to a separate class and made
    **                           many code formatting cleanups.
    ** 017 NVS 20061011   @30321 Fixed a bug in getAllAccountIds() method that
    **                           returned an empty list of accounts when type
    **                           was specified as ACC_ALL.
    ** 018 GES 20061215   @31707 Changes in bootstrap config interface.
    ** 019 NVS 20090415   @41800 Cache generation messages can be routed to
    **                           admin client, server console or both ways.
    ** 020 NVS 20090504   @42097 Added getCaAliases() and getPeerAliases()
    **                           methods.
    ** 021 NVS 20090528   @42480 Custom account extension plugins are read
    **                           from the directory and made available through
    **                           getCustomServerExt(), getCustomClientExt().
    ** 022 NVS 20090602   @42548 Custom account extension plugin jar file name
    **                           is read from the directory and made available
    **                           through getCustomClientJar() method.
    ** 023 NVS 20090603   @42594 Supporting account enablement status.
    ** 024 NVS 20090604   @42595 Supporting password protected status.
    ** 025 NVS 20090615   @42696 Removed #022 due to a utilization of a simpler
    **                           approach.
    ** 026 NVS 20090624   @42844 Massively changed the ACL creation logic due to
    **                           move to the simplified ACL representation in the
    **                           directory. Added the automatic ACL style detection
    **                           and migration logic, which accepts the old style
    **                           ACL nodes in the directory and converts them into
    **                           their simplified new style equivalents, on the
    **                           fly.
    ** 027 NVS 20090701   @43005 Creation of rights is done only after calling
    **                           isRightsSetValid() plugin's method successfully.
    ** 028 NVS 20090824   @43727 Added a package private constructor for use in
    **                           standalone security/directory utilites. It mimics
    **                           the server environment to a degree sufficient to
    **                           implement utility functions: reads accounts and
    **                           loads the specified resource plugin.
    ** 029 NVS 20090825   @43737 Standalone environment also reads certificates.
    ** 030 NVS 20090826   @43774 Lowered log level for the "No holidays defined"
    **                           message.
    ** 031 NVS 20090915   @43914 Fixed a bug in the readUsers() method not clearing
    **                           groups local variable in the reading loop.
    ** 032 SIY 20100419   @44823 Implemented automatic adding of function into
    **                           directory if it does not exists.
    ** 033 SVL 20100916          Added support for multiple custom auth plugins
    **                           and auth mode inheritance.
    ** 034 LMR 20101217          Modified the constructors, readProcesses(),
    **                           readUsers() and verifyServerId() to attempt to
    **                           verify the alias of an account (process, user,
    **                           server) by looking up a certificate for it in
    **                           the trust store only if net/connection/secure
    **                           option is set to true in the bootstrap config
    **                           (defaults to false if it's missing).
    ** 035 GES 20111109          Added getUniqueInitialSecurityContext() as a
    **                           helper to create and obtain a unique context that
    **                           is used as the initial context for a thread that
    **                           should not share its context with other server
    **                           threads.
    ** 036 CA  20130529          Added appserver support.
    ** 037 CA  20130704          Added API to get the accounts for a specified appserver.
    ** 038 MAG 20131219          Added API to update user cached accounts.
    ** 039 CA  20140206          Changed H036: appserver agents can be started only from P2J processes.
    **                           Implemented in-directory private keys.
    ** 111 MAG 20140715          Added broker property to process account.
    ** 112 ECF 20150715          Replace StringBuffer with StringBuilder.
    ** 113 EVL 20160224          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 114 CA  20160106          Log a warning if the loaded certificates use a RSA key length less
    **                           than 2048 bits.  Also, if the certificate is signed using a SHA-1
    **                           algorithm, an error is logged.
    ** 115 SBI 20170225          Removed DataValue and TimeValue fields out of UserDef to be GWT
    **                           serializable.
    ** 116 SBI 20171116          Changed constructor in order to fill web store from the directory,
    **                           added getWebStore() and getWebServerAlias().
    ** 117 IAS 20210505          Added check for the certificate uniqueness.
    **     GES 20210902          Added settings to limit simultaneous interactive user sessions.
    **     CA  20220405          Added authentication and authorization for web requests.  When this is enabled,
    **                           the target API call will be executed under the authenticated FWD context, and not
    **                           the agent's context.
    **     TJD 20220504          Java 11 compatibility minor changes
    ** 118 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 119 GBB 20230523          Change the logger level to WARNING if no configs have been set.
    ** 120 GBB 20230825          Adding support for OS users.
    ** 121 GBB 20240730          Remove the implicit dependency on DirectoryManager by replacing
    **                           EnvironmentOps.isUnderWindowsFamily with PlatformHelper.
    ** 122 RAA 20241216          If there are multiple web certificates with the same alias, throw an exception.
    **     RAA 20241216          Added hint in the multiple web certificates aliases error message.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.security;

    import java.io.*;
    import java.math.*;
    import java.security.*;
    import java.security.cert.*;
    import java.security.interfaces.*;
    import java.util.*;
    import java.util.logging.*;

    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.admin.*;
    import com.goldencode.p2j.security.SSLCertFactory.CertificateSuite;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;
    import com.goldencode.util.*;

    /**
     * Security cache is a collection of control data that originates in the P2J
     * directory and/or the bootstrap configuration and is maintained in memory
     * for quick access. Pieces of the cache are:
     * <p>
     * <ul>
     *   <li>truststore which holds all certificates;
     *   <li>authentication related data (contents of the authMode directory
     *       object);
     *   <li>process, user and group accounts;
     *   <li>resource plugins configuration;
     *   <li>access control lists
     * </ul>
     * <p>
     * Only the first item is common between the client and server sides. The rest
     * of the list is for the server side only.
     */
    class SecurityCache
    implements SecurityConstants
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(SecurityCache.class);

       /** logging mode for admin client */
       public final static int LM_ADMIN = 0;

       /** logging mode for server console */
       public final static int LM_CONSOLE = 1;

       /** logging mode for admin client and server console */
       public final static int LM_DUAL = 2;

       /** logging mode for this cache instance */
       private final int logMode;

       /** Serial (generation) number of this instance of cache. */
       private int serialNo = 0;

       /** In-memory truststore. */
       private KeyStore trustStore = null;

       /** The key store used for https, defined by webCertificates */
       private KeyStore webStore;

       /** The web server's store alias, defined by webCertificates/alias */
       private String webServerAlias;

       /** CA certificate aliases in truststore */
       private String[] caAliases = null;

       /** Peer certificate aliases in truststore */
       private String[] peerAliases = null;

       /** All known accounts. */
       private Account[] accs = null;

       /** All known OS users. */
       private OsUserAccount[] osUsers = null;

       /** Server's ID. */
       private String serverId = null;

       /** Server's process account. */
       private ProcessAccount serverAcc = null;

       /** Server's truststore alias. */
       private String serverAlias = null;

       /** Initial security context. */
       private SecurityContext initContext = null;

       /** Authorization mode. */
       private int authMode = 0;

       /**
        * The number of authorization retries, where:
        *   -1  unlimited retries
        *    0  no retries
        *  &gt; 0  specific limit
        */
       private int authRetries = 0;

       /**
        * Set of available auth plugins (plugin class -&gt; plugin description
        * object).
        */
       private Map<String, AuthPlugin> authPlugins = null;

       /** Authorization hook class name. */
       private String hookName = null;

       /** SSO authorization hook. */
       private SsoAuthenticator ssoHook = null;

       /** Account ID for anonymous processes or <code>null</code> to disallow. */
       private String anonymId = null;

       /** Registry of abstract resource plugins. */
       private ResourceRegistry[] registry = null;

       /** Ordinal number of the "system" resource in registry. */
       private int systemOrdinal = -1;

       /** Set of holidays. */
       private Set<String> holidays = null;

       /** Audit control. */
       private Audit audit = null;

       /** Instance of symbol resolver. */
       private Resolver resolver = null;

       /** Maximum number of days user password is valid; 0 means unlimited. */
       private int maxAge = 0;

       /** Password input hook. */
       private String pwInputHook = null;

       /** Maximum number of simultaneous interactive user accounts, -1 means unlimited. */
       private int maxInteractiveUsers = -1;

       /** Maximum number of simultaneous sessions per each interactive user, -1 means unlimited. */
       private int maxSessionsPerUser = -1;

       /** instance of security manager */
       private SecurityManager sm = null;

       /** buffer to receive messages */
       List<String[]> msgBuf = null;

       /** Custom account extensions server plugin class name. */
       private String customServerExt = null;

       /** Custom account extensions client plugin class name. */
       private String customClientExt = null;

       /** Encryption passwords for each SSL private key, per alias. */
       private Map<String, String> privateKeyPasswords = new HashMap<>();

       /** Encrypted SSL private keys, per alias.*/
       private Map<String, byte[]> privateKeys = new HashMap<>();

       /** The SSL certificate factory. */
       private SSLCertFactory sslFactory = null;

       static
       {
          // sets the default logger level to WARNING, if no configs have set the logger level yet
          if (LOG.getExplicitLevel() == null)
          {
             LOG.setLevel(Level.WARNING);
          }
       }

       /**
        * Package private constructor.
        *
        * @param    bc
        *           An instance of <code>BootstrapConfig</code> class used to get
        *           the security related configuration information.
        * @param    newSerial
        *           Serial number for the generation of the security cache being
        *           created.
        */
       SecurityCache(BootstrapConfig bc, int newSerial)
       throws ConfigurationException,
              NoSuchMethodException
       {
          this(bc, newSerial, LM_CONSOLE, null);
       }

       /**
        * Package private constructor.
        *
        * @param    bc
        *           An instance of <code>BootstrapConfig</code> class used to get
        *           the security related configuration information.
        * @param    newSerial
        *           Serial number for the generation of the security cache being
        *           created.
        * @param    logMode
        *           specifies logging for the admin client, or server console,
        *           or both
        * @param    msgBuf
        *           list to receive messages if logmode enables admin messages
        */
       SecurityCache(BootstrapConfig bc, int newSerial, int logMode, List<String[]> msgBuf)
       throws ConfigurationException,
              NoSuchMethodException
       {
          this.logMode = logMode;
          this.msgBuf  = msgBuf;

          // security cache is a server-side only initialization
          if (!bc.isServer())
             return;

          sm = SecurityManager.getInstance();

          // get this server ID
          serverId = bc.getConfigItem("security", "server", "id");

          if (serverId == null)
             throw new ConfigurationException("no server ID defined");

          // access directory
          DirectoryService ds = DirectoryService.getInstance();

          boolean secure = bc.getBoolean("net", "connection", "secure", false);
          if(secure)
          {
             // create truststore
             try
             {
                trustStore = KeyStore.getInstance("JKS");
                trustStore.load(null, null);
             }
             catch (Exception e)
             {
                throw new ConfigurationException("can't create empty truststore", e);
             }

             // read from /security/certificates/CAs into the truststore
             readCAs(trustStore, ds);

             // verify that all CA certificates are valid and resolvable into a chain
             verifyCAs(trustStore);

             // read from /security/certificates/peers into the truststore
             // these are verified while reading
             readPeers(trustStore, ds);

             // read the certificates' private keys, their passwords and other configuration
             readPrivateKeys(bc, ds);
             readRSAConfig(bc, ds);
          }

          // read from /security/config/auth-mode
          readAuthMode(ds);

          //
          // read all accounts
          //

          // read from /security/accounts/osusers
          osUsers = readOsUsers(ds);

          List<Account> accounts = new LinkedList<>();

          // read from /security/accounts/groups
          readGroups(ds, accounts);

          // convert the list into a temporary array
          accs = new Account[accounts.size()];
          accs = accounts.toArray(accs);

          // read from /security/accounts/processes
          readProcesses(ds, accounts, secure);

          // read from /security/accounts/users
          readUsers(ds, accounts, secure);

          // convert the list into an array
          accs = new Account[0];
          accs = accounts.toArray(accs);
          accounts = null;

          // verify that the server ID in bootstrap configuration matches a server
          // account
          verifyServerId(secure);

          // read list of holidays
          holidays = new HashSet<String>();
          readHolidays(ds);

          // read password aging parameters
          readPasswordAging(ds);

          // read user session limits
          readInteractiveUserLimits(ds);

          // verify the anonymous process account
          verifyAnonymousId();

          // read and register abstract resource plugins
          readPlugins(ds, null);
          systemOrdinal = getRegistryOrdByTypeName("system");
          if (systemOrdinal == -1)
             throw new ConfigurationException(
                       "no plugin defined for system resource");

          // read custom server extension name
          customServerExt = readCustomExtension(ds,
                            "/security/config/extensions/server");
          if (customServerExt == null)
          {
             dWarn("No custom server extension defined in P2J directory");
          }
          else
          {
             dList("Using custom server extention " + customServerExt);
          }

          // read custom client extension name
          customClientExt = readCustomExtension(ds,
                            "/security/config/extensions/client");
          if (customClientExt == null)
          {
             dWarn("No custom client extension defined in P2J directory");
          }
          else
          {
             dList("Using custom client extention " + customClientExt);
          }

          // read and organize ACLs
          readACLs(ds);

          // read and organize audit related configuration
          readAudit(ds);

          // create the initial security context
          initContext = new SecurityContext(this, getOrdinalById(serverId), -1,
                                            null, registry.length, true);

          serialNo = newSerial;

          try
          {
             WebCertificates reader = new WebCertificates(getSSLCertFactory());

             webServerAlias = reader.getWebAlias(ds, serverId);

             if (webServerAlias != null && !webServerAlias.trim().isEmpty())
             {
                CertificateSuite suite = reader.getWebCertificates(ds, serverId);

                String keyPassword = SSLCertGenUtil.createAES256BitKey();

                byte[] encrypted = getSSLCertFactory().encryptPrivateKey(suite.privateKey, keyPassword);

                if (privateKeys.putIfAbsent(webServerAlias, encrypted) != null)
                {
                   throw new Exception("The web certificate alias " +
                                       webServerAlias +
                                       " already exists. Check both web and non-web certificate aliases. ");
                }

                privateKeyPasswords.put(webServerAlias, keyPassword);

                webStore = SSLCertGenUtil.createEmptyStore();

                webStore.setKeyEntry(webServerAlias,
                                     suite.privateKey,
                                     keyPassword.toCharArray(),
                                     suite.getFullChain());
             }
          }
          catch (Exception e)
          {
             StringWriter writer = new StringWriter();

             new ConfigurationException("can't load web certificates", e).printStackTrace(
                      new PrintWriter(writer));

             dWarn(writer.getBuffer().toString());
          }
       }

       /**
        * Package private constructor to mimic the server environment for
        * standalone utilities.
        *
        * @param    logMode
        *           specifies logging for the admin client, or server console,
        *           or both
        * @param    msgBuf
        *           list to receive messages if logmode enables admin messages
        * @param    plugins
        *           array of resource plugins to be loaded or <code>null</code>
        */
       SecurityCache(int logMode, List<String[]> msgBuf, String[] plugins)
       throws ConfigurationException,
              NoSuchMethodException
       {
          this.logMode = logMode;
          this.msgBuf  = msgBuf;

          sm = SecurityManager.getInstance();

          // create fake server name and account
          serverId = "standalone";
          serverAcc = new ProcessAccount(serverId, true, "simulation", serverId, true, true, null, null);

          DirectoryService ds = DirectoryService.getInstance();
          // create truststore
          try
          {
             trustStore = KeyStore.getInstance("JKS");
             trustStore.load(null, null);
          }
          catch (Exception e)
          {
             throw new ConfigurationException("can't create empty truststore", e);
          }

          // read from /security/certificates/CAs into the truststore
          readCAs(trustStore, ds);

          // verify that all CA certificates are valid and resolvable into a chain
          verifyCAs(trustStore);

          // read from /security/certificates/peers into the truststore
          // these are verified while reading
          readPeers(trustStore, ds);

          // load resource plugins
          if (plugins != null)
          {
             readPlugins(ds, plugins);
          }

          //
          // read all accounts
          //

          List<Account> accounts = new LinkedList<>();

          // read from /security/accounts/groups
          readGroups(ds, accounts);

          // convert the list into a temporary array
          accs = new Account[accounts.size()];
          accs = accounts.toArray(accs);

          // read from /security/accounts/processes
          readProcesses(ds, accounts, false);

          // read from /security/accounts/users
          readUsers(ds, accounts, false);

          // convert the list into an array
          accs = new Account[0];
          accs = accounts.toArray(accs);
          accounts = null;
       }

       /**
        * Checks to see if the given subject ID is unique.
        *
        * @param    id
        *           Account ID to check.
        * @param    list
        *           List of defined accounts.
        *
        * @return   <code>true</code> if subject ID is unique.
        */
       private static boolean checkSubjectId(String id, List<Account> list)
       {
          Iterator<Account> it = list.iterator();
          Account acc = null;

          while (it.hasNext())
          {
             acc = it.next();
             if (acc.getSubjectId().compareToIgnoreCase(id) == 0)
                return false;
          }

          return true;
       }

       /**
        * Gets this security cache's serial number.
        *
        * @return   Serial number.
        */
       int getSerial()
       {
          return serialNo;
       }

       /**
        * Refresh all plugins.
        *
        * @param ae
        */
       void refreshAll(AdminAccountExtension ae)
       {
          for (ResourceRegistry resource : registry)
          {
             resource.getPlugin().refresh(ae);
          }
       }

       /**
        * Verifies the validity of certification authority certificates loaded
        * into the trust store.
        * <p>
        * CA certificates have to:
        * <ul>
        *    <li>be within the validity dates;
        *    <li>have CA:TRUE in X509v3 Basic Constraints;
        *    <li>be either self-signed or signed by another valid CA
        *        from the trust store;
        * </ul>
        * Non-compliant certificates are deleted from the trust store.
        *
        * @param    trustStore
        *           Truststore loaded with CA certificates to verify.
        *
        * @throws   ConfigurationException
        *           If the trust store is or becomes empty.
        */
       void verifyCAs(KeyStore trustStore)
       throws ConfigurationException
       {
          // TODO: not yet implemented
          if (trustStore == null)
             throw new ConfigurationException("");
       }

       /**
        * Returns the trust store.
        *
        * @return   Current truststore.
        */
       KeyStore getTrustStore()
       {
          return trustStore;
       }

       /**
        * Returns the web store.
        *
        * @return   Current web store.
        */
       KeyStore getWebStore()
       {
          return webStore;
       }

       /**
        * Gets the web server certificate alias.
        *
        * @return   The web server certificate alias
        */
       String getWebServerAlias()
       {
          return webServerAlias;
       }

       /**
        * Gets authorization mode.
        *
        * @return   Authorization mode.
        */
       int getAuthMode()
       {
          return authMode;
       }

       /**
        * Gets the maximum number of authorization retries.
        *
        * @return   Authorization retry limit.
        */
       int getAuthNumRetries()
       {
          return authRetries;
       }

       /**
        * Gets anonymous account ID.
        *
        * @return   Anonymous account ID.
        */
       String getAnonymId()
       {
          return anonymId;
       }

       /**
        * Gets custom server extension plugin name.
        *
        * @return  custom server extension plugin name
        */
       String getCustomServerExt()
       {
          return customServerExt;
       }

       /**
        * Gets custom client extension plugin name.
        *
        * @return  custom client extension plugin name
        */
       String getCustomClientExt()
       {
          return customClientExt;
       }

       /**
        * Gets authentication hook name.
        *
        * @return   Authentication hook name.
        */
       String getHookName()
       {
          return hookName;
       }

       /**
        * Gets SSO authentication hook.
        *
        * @return   SSO authentication hook.
        */
       SsoAuthenticator getSsoHook()
       {
          return ssoHook;
       }

       /**
        * Gets the initial security context.
        *
        * @return   Initial server's <code>SecurityContext</code>.
        */
        SecurityContext getInitialSecurityContext()
        {
           return initContext;
        }

       /**
        * Gets a new and unique instance of an initial security context.
        *
        * @return   Unique initial <code>SecurityContext</code> which is not
        *           associated with any account.
        */
        SecurityContext getUniqueInitialSecurityContext()
        {
          return new SecurityContext(this, -1, -1, null, registry.length, true);
        }

       /**
        * Gets an OS user by its ID. Comparison is case-insensitive.
        *
        * @param    id
        *           OS user ID.
        *
        * @return   <code>OsUserAccount</code></code> object or <code>null</code> if not found.
        */
       OsUserAccount getOsUserById(String id)
       {
          if (id == null)
             return null;

          if (PlatformHelper.isUnderWindowsFamily())
          {
             for (int i = 0; i < osUsers.length; i++)
             {
                if (osUsers[i].getSubjectId().compareTo(id) == 0)
                   return osUsers[i];
             }
          }
          else
          {
             for (int i = 0; i < osUsers.length; i++)
             {
                if (osUsers[i].getSubjectId().compareToIgnoreCase(id) == 0)
                   return osUsers[i];
             }
          }

          return null;
       }

       /**
        * Gets all OS users.
        *
        * @return   An array of <code>OsUserAccount</code></code> objects.
        */
       OsUserAccount[] getAllOsUsers()
       {
          return osUsers;
       }

       /**
        * Gets an account by its ID.
        * <p>
        * Comparison is case-insensitive.
        *
        * @param    id
        *           Account ID.
        *
        * @return   <code>Account</code> object or <code>null</code> if not found.
        */
       Account getAccountById(String id)
       {
          if (id == null)
             return null;

          for (int i = 0; i < accs.length; i++)
          {
             if (accs[i].getSubjectId().compareToIgnoreCase(id) == 0)
                return accs[i];
          }

          return null;
       }

       /**
        * Get the process accounts configured to start the given appserver.  If the
        * <code>appServer</code> is <code>null</code>, this will return an array of length 0.
        *
        * @param    appServer
        *           The appserver for which the accounts are needed.
        *
        * @return   The process accounts configured for this appServer, if any.
        */
       String[] getAccountsForAppserver(String appServer)
       {
          if (appServer == null)
          {
             return new String[0];
          }

          List<String> subjects = new ArrayList<String>();

          for (Account a : accs)
          {
             if (a instanceof ProcessAccount)
             {
                ProcessAccount pa = (ProcessAccount) a;

                if (appServer.equals(pa.getAppServer()))
                {
                   subjects.add(a.getSubjectId());
                }
             }
          }

          return subjects.toArray(new String[0]);
       }

       /**
        * Gets IDs of all defined accounts of the specified type.
        *
        * @param    type
        *           Account type.
        *
        * @return   The array of all defined account IDs of the type.
        */
       public String[] getAllAccountIds(int type)
       {
          List<String> l = new LinkedList<>();

          // scan the array of accounts and select only those of the given type
          for (int i = 0; i < accs.length; i++)
          {
             if (accs[i].getAccountType() != type && type != Account.ACC_ALL)
                continue;

             l.add(accs[i].getSubjectId());
          }

          // convert the list of IDs into an array
          String[] ids = l.toArray(new String[] {});
          return ids;
       }

       /**
        * Gets the number of user or process accounts by a trust store alias.
        * <p>
        * Comparison is case-insensitive.
        *
        * @param    alias
        *           Trust store alias.
        * @param    type
        *           Account type.
        *
        * @return   <code>Account</code> object or <code>null</code> if not
        *           found.
        */
       int getCountByAlias(String alias, int type)
       {
          if (alias == null)
             return 0;

          int n = 0;

          for (int i = 0; i < accs.length; i++)
          {
             if (accs[i].getAccountType() != type)
                continue;

             if (accs[i].getAlias() != null &&
                 accs[i].getAlias().compareToIgnoreCase(alias) == 0)
                n++;
          }

          return n;
       }

       /**
        * Gets an array of user or process accounts by a trust store alias. In
        * general, there may be more than one account sharing a trust store alias.
        * <p>
        * Comparison is case-insensitive.
        *
        * @param    alias
        *           Trust store alias.
        * @param    type
        *           Account type.
        *
        * @return   Array of accounts.
        */
       Account[] getAccountsByAlias(String alias, int type)
       {
          if (alias == null)
             return null;

          List<Account> list = new LinkedList<>();

          for (int i = 0; i < accs.length; i++)
          {
             if (accs[i].getAccountType() != type)
                continue;

             if (accs[i].getAlias() != null &&
                 accs[i].getAlias().compareToIgnoreCase(alias) == 0)
                list.add(accs[i]);
          }

          return list.toArray(new Account[0]);
       }

       /**
        * Gets account's ordinal number by its ID.
        *
        * @param    id
        *           Account ID.  {@code null} or empty string will not match any account.
        *
        * @return   Ordinal number or -1 if not found.
        */
       int getOrdinalById(String id)
       {
          if (id == null || id.length() == 0)
          {
             return -1;
          }

          for (int i = 0; i < accs.length; i++)
          {
             if (accs[i].getSubjectId().compareToIgnoreCase(id) == 0)
             {
                return i;
             }
          }

          return -1;
       }

       /**
        * Gets an account by its ordinal number.
        *
        * @param    ordinal
        *           Account ordinal.
        *
        * @return   <code>Account</code> object or <code>null</code>.
        */
       Account getAccountByOrd(int ordinal)
       {
          if (ordinal < 0 || ordinal > accs.length - 1)
             return null;
          else
             return accs[ordinal];
       }

       /**
        * Gets the number of registered resource plugins.
        *
        * @return   Number of registered resource plugins.
        */
       int getNumResources()
       {
          if (registry == null)
             return 0;
          else
             return registry.length;
       }

       /**
        * Gets a registry ordinal by the resource type name.
        * <p>
        * Comparison is case-sensitive.
        *
        * @param    typeName
        *           Abstract resource type name.
        *
        * @return   Index of the plugin which provides the named resource or -1
        *           for no match.
        */
       int getRegistryOrdByTypeName(String typeName)
       {
          if (typeName == null)
             return -1;

          for (int i = 0; i < registry.length; i++)
          {
             if (registry[i].getTypeName().equals(typeName))
                return i;
          }

          return -1;
       }

       /**
        * Gets a registry entry by its ordinal number.
        *
        * @param    ordinal
        *           Index (ordinal number) of the abstract resource.
        *
        * @return   <code>ResourceRegistry</code> registered under this index or
        *           <code>null</code>.
        */
       ResourceRegistry getRegistryByOrd(int ordinal)
       {
          if (ordinal < 0 || ordinal > registry.length - 1)
             return null;

          return registry[ordinal];
       }

       /**
        * Gets the defined set of holidays.
        *
        * @return   <code>Set</code> containing dates of all defined holidays as
        *           <code>String</code>s formatted to "yyyy-mm-dd".
        */
       Set<String> getHolidays()
       {
          return holidays;
       }

       /**
        * Gets the Audit object from this cache.
        *
        * @return   <code>Audit</code> defined in this cache.
        */
       Audit getAudit()
       {
          return audit;
       }

       /**
        * Gets the system resource ordinal number.
        *
        * @return   System ordinal number.
        */
       int getSystem()
       {
          return systemOrdinal;
       }

       /**
        * Gets this server's account.
        *
        * @return   Server's account.
        */
       ProcessAccount getServerAccount()
       {
          return serverAcc;
       }

       /**
        * Gets the symbol resolver.
        *
        * @return   <code>Resolver</code> object.
        */
       Resolver getResolver()
       {
          return resolver;
       }

       /**
        * Gets the maximum password age in days.
        *
        * @return   Maximum allowed password age where 0 means unlimited age.
        */
       int getMaxAge()
       {
          return maxAge;
       }

       /**
        * Gets the maximum number of simultaneous interactive users.
        *
        * @return   Maximum allowed simultaneous interactive users where -1 means there is no limit.
        */
       int getMaxInteractiveUsers()
       {
          return maxInteractiveUsers;
       }

       /**
        * Gets the maximum number of simultaneous sessions per interactive user.
        *
        * @return   Maximum allowed simultaneous sessions per interactive user where -1 means there is no limit.
        */
       int getMaxSessionsPerUser()
       {
          return maxSessionsPerUser;
       }

       /**
        * Gets the password input hook class name.
        *
        * @return   Class name for the preconfigured password input hook.
        */
       String getPasswordInput()
       {
          return pwInputHook;
       }

       /**
        * Gets the array of CA certificate aliases.
        *
        * @return array of CA certificate aliases
        */
       String[] getCaAliases()
       {
          return caAliases;
       }

       /**
        * Gets the array of peer certificate aliases.
        *
        * @return array of peer certificate aliases
        */
       String[] getPeerAliases()
       {
          return peerAliases;
       }

       /**
        * Get the encryption password for the SSL private key associated with the given alias.
        * <p>
        * Only the server can access this.
        *
        * @param    alias
        *           The alias.
        *
        * @return   See above.
        */
       String getKeyEntryPassword(String alias)
       {
          if (!(serverAcc.isServer() && serverAcc.isMaster()))
          {
             return null;
          }

          return privateKeyPasswords.get(alias);
       }

       /**
        * Get the factory used to create SSL private keys and certificates.
        *
        * @return   The {@link BCCertFactory Bouncy Castle} {@link #sslFactory factory}.
        *
        * @throws   SSLCertGenException
        *           If an exception occurs during its instantiation.
        */
       SSLCertFactory getSSLCertFactory()
       throws SSLCertGenException
       {
          if (sslFactory == null)
          {
             sslFactory = new BCCertFactory();
          }

          return sslFactory;
       }

       /**
        * Get the encrypted SSL private key associated with the given alias.
        *
        * @param    alias
        *           The alias.
        *
        * @return   See above.
        */
       byte[] getPrivateKey(String alias)
       {
          return privateKeys.get(alias);
       }

       /**
        * Get auth plugin description object by its class.
        *
        * @param   pluginClass
        *          Class of the required plugin.
        *
        * @return  auth plugin description object.
        */
       AuthPlugin getAuthPlugin(String pluginClass)
       {
          return authPlugins.get(pluginClass);
       }

       /**
        * Get the list of available auth plugins.
        *
        * @return  See above.
        */
       AuthPlugin[] getAuthPlugins()
       {
          return authPlugins.values().toArray(new AuthPlugin[authPlugins.size()]);
       }

       /**
        * Reads the P2J directory and loads CA certificates into truststore.
        *
        * @param    trustStore
        *           The target trust store.
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        */
       private void readCAs(KeyStore trustStore, DirectoryService ds)
       {
          try
          {
             CertificateFactory cf = CertificateFactory.getInstance("X.509");
             X509Certificate cert = null;
             byte[] data = null;
             ByteArrayInputStream bis = null;
             String fullId = null;
             String path = "/security/certificates/CAs";

             // enumerate CA certificates
             String[] cas = ds.enumerateNodes(path);
             int csize = cas == null ? 0 : cas.length;
             if (csize == 0)
             {
                cas = new String[] {};
                dWarn("No CA certificates defined in the P2J directory");
             }

             // keep the list of aliases
             caAliases = cas;

             int stats = 0;
             int statf = 0;

             // process existing nodes
             for (int i = 0; i < csize; i++)
             {
                fullId = new String(path + "/" + cas[i]);
                if (!ds.getNodeClass(fullId).equals("/meta/class/bytes"))
                {
                   dList("Object " + fullId + " ignored");
                   statf++;
                   continue;
                }

                data = ds.getNodeByteArray(fullId, "value");
                bis  = new ByteArrayInputStream(data);
                cert = (X509Certificate)cf.generateCertificate(bis);

                checkCertificate(trustStore, cas[i], cert);

                trustStore.setCertificateEntry(cas[i], cert);
                dList("Loaded CA   cert of " +
                         sm.certificateSm.getSubjectCommonName(cert));
                stats++;
             }

             dStat("Loaded " + stats + " CA   certificates, " + statf +
                      " objects ignored");
          }
          catch (Exception e)
          {
             dErr("Exception while reading CA   certificates " + e);
          }
       }

       /**
        * Check the RSA key length and encryption algorithm for the given certificate. A warning is
        * logged if the RSA key length is less than {@link SSLCertFactory#MIN_RSA_KEY_STRENGTH} bits.
        * Also, an error is logged if the encryption algorithm is SHA-1.
        *
        * @throws   CertificateParsingException
        *           If the certificate's common-name can not be determined.
        * @throws   KeyStoreException
        *           If keystore lookup by certificate failed.
        */
       private void checkCertificate(KeyStore trustStore, String alias, X509Certificate cert)
       throws CertificateParsingException,
              KeyStoreException
       {
          String existing = trustStore.getCertificateAlias(cert);
          if (existing != null)
          {
             if (!existing.equals(alias))
             {
                LOG.warning(String.format(
                         "Identical certificates found for aliases [%s] and [%s]. " +
                         "Certificates MUST NOT be used for more than one alias",
                         alias, existing
                ));
             }
          }
          PublicKey pkey = cert.getPublicKey();

          if (pkey instanceof RSAPublicKey)
          {
             RSAPublicKey rsaPk = (RSAPublicKey) pkey;
             int keyLength = rsaPk.getModulus().bitLength();

             if (keyLength < SSLCertFactory.MIN_RSA_KEY_STRENGTH)
             {
                String cn = sm.certificateSm.getSubjectCommonName(cert);
                String msg = "Certificate alias [%s] for [CN=%s] was signed with a RSA key of " +
                             "length %d. A minimum key length of %d is recommended.";
                dWarn(String.format(msg,  alias, cn, keyLength, SSLCertFactory.MIN_RSA_KEY_STRENGTH));
             }
          }

          String algName = cert.getSigAlgName();
          if (algName.startsWith("SHA1"))
          {
             String cn = sm.certificateSm.getSubjectCommonName(cert);
             String msg = "Certificate alias [%s] for [CN=%s] was signed using the %s algorithm. " +
                          "Use a SHA-2 implementation instead.";
             dErr(String.format(msg, alias, cn, algName));
          }
       }

       /**
        * Reads the P2J directory and loads peers certificates into truststore.
        * <p>
        * Peer certificates are verified before they are put into the trust store.
        * They have to:
        * <ul>
        *    <li>be within the validity dates;
        *    <li>be signed by a CA, not self-signed;
        * </ul>
        *
        * @param    trustStore
        *           The target trust store.
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        */
       private void readPeers(KeyStore trustStore, DirectoryService ds)
       {
          try
          {
             CertificateFactory cf = CertificateFactory.getInstance("X.509");
             X509Certificate cert = null;
             byte[] data = null;
             ByteArrayInputStream bis = null;
             String fullId = null;
             String path = "/security/certificates/peers";

             // enumerate peer certificates
             String[] peers = ds.enumerateNodes(path);
             int psize = peers == null ? 0 : peers.length;
             if (psize == 0)
             {
                peers = new String[] {};
                dWarn("No peer certificates defined in the P2J directory");
             }

             // keep the list of aliases
             peerAliases = peers;

             int stats = 0;
             int statf = 0;

             // process existing nodes
             for (int i = 0; i < psize; i++)
             {
                fullId = new String(path + "/" + peers[i]);
                if (!ds.getNodeClass(fullId).equals("/meta/class/bytes"))
                {
                   dList("Object " + fullId + " ignored");
                   statf++;
                   continue;
                }

                // read the certificate data from directory
                data = ds.getNodeByteArray(fullId, "value");
                bis  = new ByteArrayInputStream(data);
                cert = (X509Certificate)cf.generateCertificate(bis);

                checkCertificate(trustStore, peers[i], cert);

                // verify the certificate
                // to be implemented

                // add the certificate to the trust store
                trustStore.setCertificateEntry(peers[i], cert);
                dList("Loaded peer cert of " +
                         sm.certificateSm.getSubjectCommonName(cert));
                stats++;
             }

             dStat("Loaded " + stats + " peer certificates, " + statf +
                      " objects ignored");
          }
          catch (Exception e)
          {
             dErr("Exception while reading peer certificates " + e);
          }
       }

       /**
        * Read the RSA configuration.
        *
        * @param    cfg
        *           An instance of <code>BootstrapConfig</code> class used to get
        *           the security related configuration information.
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        */
       private void readRSAConfig(BootstrapConfig cfg, DirectoryService ds)
       {
          String root = "/security/certificates/company/";
          Integer strength = null;
          BigInteger exponent = null;

          try
          {
             strength = ds.getNodeInteger(root + "rsaKeyStrength", "value");
             String exp = ds.getNodeString(root + "rsaPublicExponent", "value");
             if (exp != null)
             {
                exponent = new BigInteger(exp);
             }
          }
          catch (Exception e)
          {
             dErr("Exception while reading RSA configuration " + e);
          }

          try
          {
             sslFactory = getSSLCertFactory();
             sslFactory.init(strength, exponent);
          }
          catch (SSLCertGenException e)
          {
             throw new RuntimeException("Could not instantiate the SSL factory!", e);
          }
       }

       /**
        * Read the SSL private keys and their encryption keys.
        *
        * @param    cfg
        *           An instance of <code>BootstrapConfig</code> class used to get
        *           the security related configuration information.
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        */
       private void readPrivateKeys(BootstrapConfig cfg, DirectoryService ds)
       {
          try
          {
             String path = "/security/certificates/private-keys/";
             String[] aliases = ds.enumerateNodes(path);
             if (aliases == null)
             {
                dWarn("No private keys defined in the P2J directory");
                return;
             }

             for (String alias : aliases)
             {
                String aid = path + "/" + alias;
                String kpId = aid + "/key-password";
                String keId = aid + "/key-entry";

                if (!ds.getNodeClass(keId).equals("/meta/class/bytes"))
                {
                   dList("Object " + aid + " ignored");
                   continue;
                }

                String kp = null;

                if (ds.getNodeClass(kpId) == null)
                {
                   kp = cfg.getString("access", "password", "masterkeyentry", null);
                   if (kp == null)
                   {
                      // falback to per-alias password
                      kp = cfg.getString("access", "password", "keyentry-" + alias, null);
                      if (kp == null)
                      {
                         dList("Object " + aid + " ignored - no masterkeyentry or " +
                               "keyentry-" + alias + " password provided");
                         continue;
                      }
                   }
                }
                else
                {
                   byte[] kpData = ds.getNodeByteArray(kpId, "value");
                   kp = new String(kpData);
                }

                byte[] keData = ds.getNodeByteArray(keId, "value");

                privateKeyPasswords.put(alias, kp);
                privateKeys.put(alias, keData);

                dList("Loaded private key for alias " + alias);
             }
          }
          catch (Exception e)
          {
             dErr("Exception while reading SSL private keys " + e);
          }
       }

       /**
        * Reads the P2J directory and loads defined group accounts.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        * @param    list
        *           List to which to add new accounts.
        */
       private void readGroups(DirectoryService ds, List<Account> list)
       {
          String text = null;
          String fullId = null;
          String path = "/security/accounts/groups";

          // enumerate group accounts
          String[] groups = ds.enumerateNodes(path);

          int gsize = groups == null ? 0 : groups.length;

          if (gsize == 0)
          {
             dWarn("No user groups defined in the P2J directory");
             return;
          }

          int stats = 0;
          int statf = 0;

             // process existing nodes
          for (int i = 0; i < gsize; i++)
          {
             fullId = new String(path + "/" + groups[i]);
             if (!ds.getNodeClass(fullId).equals("/meta/class/group"))
             {
                dList("Object " + fullId + " ignored");
                statf++;
                continue;
             }

             text               = ds.getNodeString(fullId, "description");
             Integer mode       = ds.getNodeInteger(fullId, "mode");
             String authPlugin  = ds.getNodeString(fullId, "auth-plugin");
             list.add(new GroupAccount(groups[i], text, mode != null ? mode : 0, authPlugin));

             dList("Loaded group <" + groups[i] + ">");
             stats++;
          }

          dStat("Loaded " + stats + " groups, " + statf + " objects ignored");
       }

       /**
        * Reads the P2J directory and loads defined process accounts.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        * @param    list
        *           List to which to add new accounts.
        * @param    secure
        *           Specifies whether secure environment is enabled.
        */
       private void readProcesses(DirectoryService ds, List<Account> list, boolean secure)
       {
          String text = null;
          String alias = null;
          String broker = null;
          Boolean isEnabled = null;
          Boolean isServer  = null;
          Boolean isMaster  = null;
          String appServer = null;
          String fullId = null;
          String path = "/security/accounts/processes";
          boolean enabled = true;

          // enumerate process accounts
          String[] procs = ds.enumerateNodes(path);
          int psize = procs == null ? 0 : procs.length;
          if (psize == 0)
             dWarn("No processes defined in the P2J directory");

          int stats = 0;
          int statf = 0;

          // process existing nodes
          for (int i = 0; i < psize; i++)
          {
             fullId = new String(path + "/" + procs[i]);
             if (!ds.getNodeClass(fullId).equals("/meta/class/process"))
             {
                dList("Object " + fullId + " ignored");
                statf++;
                continue;
             }

             isEnabled  = ds.getNodeBoolean(fullId, "enabled");
             text     = ds.getNodeString(fullId,  "description");
             alias    = ds.getNodeString(fullId,  "alias");
             isServer = ds.getNodeBoolean(fullId, "server");
             isMaster = ds.getNodeBoolean(fullId, "master");
             appServer = ds.getNodeString(fullId, "appserver");
             broker   = ds.getNodeString(fullId, "broker");

             if (isEnabled != null)
             {
                enabled = isEnabled.booleanValue();
             }
             else
             {
                enabled = true;
             }

             // verify user alias
             if (alias != null)
             {
                if (secure)
                {
                   try
                   {
                      java.security.cert.Certificate cert =
                                         trustStore.getCertificate(alias);
                      if (cert == null)
                      {
                         // no certificate with this alias
                         // don't import this account
                         dList("Object " + fullId + " has no certificate");
                         statf++;
                         continue;
                      }
                   }
                   catch (KeyStoreException e)
                   {
                      // problem accessing certificate for this alias
                      // don't import this account
                      dList("Object " + fullId +
                               " has problems accessing certificate " + e);
                      statf++;
                      continue;
                   }
                }
             }

             // check subject ID uniqueness
             if (!checkSubjectId(procs[i], list))
             {
                // ignore non-unique account ID
                dList("Object " + fullId + " defines duplicate account ID");
                statf++;
                continue;
             }

             list.add(new ProcessAccount(procs[i], enabled, text, alias, isServer, isMaster, appServer, broker));

             dList("Loaded process <" + procs[i] + ">");
             stats++;
          }

          dStat("Loaded " + stats + " processes, " + statf +
                   " objects ignored");
       }

       /**
        * Reads the P2J directory and loads defined OS user accounts.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        *
        * @return   An array of OS user accounts defined in directory.
        */
       private OsUserAccount[] readOsUsers(DirectoryService ds)
       {
          String path = "/security/accounts/osusers";

          String[] userIds = ds.enumerateNodes(path);
          if (userIds == null || userIds.length == 0)
          {
             dWarn("No OS users defined in the P2J directory");
             return new OsUserAccount[0];
          }

          int loadedCount = 0;
          int ignoredCount = 0;

          Map<String, OsUserAccount> osUserIdAccounts = new HashMap<>();
          for (int i = 0; i < userIds.length; i++)
          {
             String subId = userIds[i];
             String fullId = path + "/" + subId;
             if (!ds.getNodeClass(fullId).equals("/meta/class/osuser"))
             {
                dList("Object " + fullId + " ignored");
                ignoredCount++;
                continue;
             }

             String description = ds.getNodeString(fullId, "description");
             Boolean isEnabled = ds.getNodeBoolean(fullId, "enabled");
             boolean enabled = isEnabled != null ? isEnabled : true;
             byte[] password = ds.getNodeByteArray(fullId, "password");
             DateValue passChangeDate = ds.getNodeDate(fullId, "pwsetdate");
             TimeValue passChangeTime = ds.getNodeTime(fullId, "pwsettime");

             // check subject ID uniqueness
             if (osUserIdAccounts.containsKey(subId.toLowerCase()))
             {
                dList("Object " + fullId + " defines duplicate OS user ID");
                ignoredCount++;
                continue;
             }

             OsUserAccount osUser = new OsUserAccount(subId,
                                                      description,
                                                      enabled,
                                                      password,
                                                      passChangeDate,
                                                      passChangeTime);
             osUserIdAccounts.put(subId.toLowerCase(), osUser);

             dList("Loaded OS user <" + userIds[i] + ">");
             loadedCount++;
          }

          dStat("Loaded " + loadedCount + " users, " + ignoredCount + " objects ignored");

          return osUserIdAccounts.values().toArray(new OsUserAccount[0]);
       }

       /**
        * Reads the P2J directory and loads defined user accounts.
        * <p>
        * Group accounts must be created by the time of this call as user
        * accounts have to be converted to contain ordinal references to groups
        * instead of IDs.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        * @param    list
        *           List to which to add new accounts.
        * @param    secure
        *           Specifies whether secure environment is enabled.
        */
       private void readUsers(DirectoryService ds, List<Account> list, boolean secure)
       {
          Boolean isEnabled = null;
          Boolean isProtected = null;
          String person = null;
          String alias = null;
          String[] groupIds = null;
          byte[] password = null;
          Integer mode = null;
          String authPlugin = null;
          String osUser = null;
          String webServiceToken = null;
          int[] groups = null;
          DateValue cdate = null;
          TimeValue ctime = null;
          int j = 0;
          String fullId = null;
          String path = "/security/accounts/users";
          boolean enabled = true;
          boolean protect = true;

          // enumerate user accounts
          String[] users = ds.enumerateNodes(path);
          int usize = users == null ? 0 : users.length;
          if (usize == 0)
             dWarn("No users defined in the P2J directory");

          int stats = 0;
          int statf = 0;

          // process existing nodes
          for (int i = 0; i < usize; i++)
          {
             fullId = new String(path + "/" + users[i]);
             if (!ds.getNodeClass(fullId).equals("/meta/class/user"))
             {
                dList("Object " + fullId + " ignored");
                statf++;
                continue;
             }

             isEnabled   = ds.getNodeBoolean(fullId, "enabled");
             isProtected = ds.getNodeBoolean(fullId, "protected");
             person   = ds.getNodeString(fullId,    "person");
             alias    = ds.getNodeString(fullId,    "alias");
             password = ds.getNodeByteArray(fullId, "password");
             cdate    = ds.getNodeDate(fullId, "pwsetdate");
             ctime    = ds.getNodeTime(fullId, "pwsettime");
             groupIds = ds.getNodeStrings(fullId,   "groups");
             mode     = ds.getNodeInteger(fullId,   "mode");
             authPlugin = ds.getNodeString(fullId, "auth-plugin");
             osUser   = ds.getNodeString(fullId, "osuser");
             webServiceToken = ds.getNodeString(fullId, "webServiceToken");

             if (isEnabled != null)
             {
                enabled = isEnabled.booleanValue();
             }
             else
             {
                enabled = true;
             }

             if (isProtected != null)
             {
                protect = isProtected.booleanValue();
             }
             else
             {
                protect = true;
             }

             int gsize = groupIds == null ? 0 : groupIds.length;

             if (gsize > 0)
             {
                groups = new int[gsize];

                // produce ordinals for group IDs
                for (j = 0; j < gsize; j++)
                   groups[j] = getOrdinalById(groupIds[j]);
             }
             else
             {
                groups = null;
             }

             // verify user alias
             if (alias != null)
             {
                if (secure)
                {
                   try
                   {
                      java.security.cert.Certificate cert =
                                         trustStore.getCertificate(alias);
                      if (cert == null)
                      {
                         // no certificate with this alias
                         // don't import this account
                         dList("Object " + fullId + " has no certificate");
                         statf++;
                         continue;
                      }
                   }
                   catch (KeyStoreException e)
                   {
                      // problem accessing certificate for this alias
                      // don't import this account
                      dList("Object " + fullId +
                               " has problems accessing certificate " + e);
                      statf++;
                      continue;
                   }
                }
             }

             // check subject ID uniqueness
             if (!checkSubjectId(users[i], list))
             {
                // ignore non-unique account ID
                dList("Object " + fullId + " defines duplicate account ID");
                statf++;
                continue;
             }

             // get effective user permisions
             boolean modeInherited = false;
             if (mode == null || mode == SecurityConstants.AUTH_MODE_NONE)
             {
                if (groups != null)
                {
                   for (int groupOrd : groups)
                   {
                      Account acc = getAccountByOrd(groupOrd);
                      if (acc != null && acc instanceof GroupAccount)
                      {
                         GroupAccount groupAccount = (GroupAccount) acc;
                         if (groupAccount.getAuthMode() !=
                             SecurityConstants.AUTH_MODE_NONE)
                         {
                            if (!modeInherited)
                            {
                               mode  = groupAccount.getAuthMode();
                               authPlugin = groupAccount.getAuthPlugin();
                               modeInherited = true;
                            }
                            else if (mode != groupAccount.getAuthMode() ||
                                (authPlugin != null &&
                                !authPlugin.equals(groupAccount.getAuthPlugin())))
                            {
                               StringBuilder sb = new StringBuilder("User ");
                               sb.append(users[i]);
                               sb.append(" consists into multiple groups with");
                               sb.append(" different auth modes");

                               LOG.warning(sb.toString());
                               break;
                            }
                         }
                      }
                   }
                }

                if (!modeInherited)
                {
                   // use server default
                   mode = authMode;
                   authPlugin = hookName;
                   modeInherited = true;
                }
             }

             UserAccount user = new UserAccount(users[i], enabled, person, alias, groups,
                                                protect, password, cdate, ctime, mode,
                                                authPlugin, osUser, modeInherited, webServiceToken);
             list.add(user);

             dList("Loaded user <" + users[i] + ">");
             stats++;
          }

          dStat("Loaded " + stats + " users, " + statf + " objects ignored");
       }

       /**
        * Reads the P2J directory and initializes authentication control data.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        *
        * @throws   ConfigurationException
        *           If the authentication mode is invalid.
        */
       private void readAuthMode(DirectoryService ds)
       throws ConfigurationException
       {
          // reading authorization mode from directory
          Integer am = ds.getNodeInteger("/security/config/auth-mode", "mode");
          if (am != null)
          {
             authMode = am.intValue();
             if (authMode < AUTH_MODE_LOWEST || authMode > AUTH_MODE_HIGHEST)
                throw new ConfigurationException("invalid authentication mode");
          }
          else
             throw new ConfigurationException("no authentication mode");

          Integer rm = ds.getNodeInteger("/security/config/auth-mode", "retries");
          if (rm != null)
          {
             authRetries = rm.intValue();
             if (authRetries < -1)
                throw new ConfigurationException("invalid retries value");
          }

          dList("Loaded auth-mode " + authMode + ", retries " + authRetries);

          // reading default authorization hook class from directory
          hookName = ds.getNodeString("/security/config/auth-mode", "plugin");
          dList("Loaded auth hook as <" + hookName + ">");

          instantiateSsoAuthenticator(ds);

          readAuthPlugins(ds);

          // reading anonymous account ID
          anonymId = ds.getNodeString("/security/config/auth-mode", "anonymous");
          if (anonymId != null && anonymId.length() == 0)
             anonymId = null;
          dList("Loaded anonymous ID <" + anonymId + ">");

          dStat("Loaded auth-mode object");
       }

       /**
        * Instantiates the SSO authentication provider.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        */
       private void instantiateSsoAuthenticator(DirectoryService ds)
       {
          // reading default SSO authorization hook class from directory
          String ssoHookName = ds.getNodeString("/security/config/auth-mode", "ssoplugin");
          String ssoHookOption = ds.getNodeString("/security/config/auth-mode", "ssopluginoption");
          dList("Loading sso auth hook as <" + ssoHookName + ">");

          if (!StringHelper.hasContent(ssoHookName))
          {
             return;
          }
          try
          {
             Class<?> ssoAuthClass = Class.forName(ssoHookName);
             ssoHook = (SsoAuthenticator) ssoAuthClass.getDeclaredConstructor().newInstance();
             ssoHook.setOption(ssoHookOption);
          }
          catch (Exception exc)
          {
             LOG.warning(
                "Can't find or can't instantiate the user-provided implementation for SsoAuthenticator.",
                exc);
          }
       }

       /**
        * Read the list of available auth plugins form the directory.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        */
       private void readAuthPlugins(DirectoryService ds)
       {
          authPlugins = new LinkedHashMap<String, AuthPlugin>();

          // enumerate all plugins
          String basePath = "/security/config/auth-plugins";

          String[] plugins = ds.enumerateNodes(basePath);
          if (plugins != null)
          {
             basePath = new StringBuilder(basePath).append("/%s").toString();

             for (String pluginId : plugins)
             {
                String pluginNodeId = String.format(basePath, pluginId);

                // get plugin class
                StringBuilder sb = new StringBuilder(pluginNodeId);
                sb.append("/classname");
                String pluginClass = ds.getNodeString(sb.toString(), "value");

                // get description
                sb = new StringBuilder(pluginNodeId);
                sb.append("/description");
                String description = ds.getNodeString(sb.toString(), "value");

                // get option
                sb = new StringBuilder(pluginNodeId);
                sb.append("/option");
                String option = ds.getNodeString(sb.toString(), "value");

                AuthPlugin authPlugin = new AuthPlugin(pluginClass,
                                                       description,
                                                       option);
                authPlugins.put(pluginClass, authPlugin);
             }
          }
       }

       /**
        * Reads the P2J directory and initializes the list of known holidays.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        */
       private void readHolidays(DirectoryService ds)
       throws ConfigurationException
       {
          int stats = 0;
          int statf = 0;

          DateValue[] hdays = ds.getNodeDates("/security/holidays", "values");
          if (hdays == null)
             dList("No holidays defined in the P2J directory.");
          else
          {
             DateValue today = new DateValue();
             for (int i = 0; i < hdays.length; i++)
             {
                if (hdays[i].compareTo(today) >= 0)
                {
                   holidays.add(hdays[i].toString());
                   dList("Loaded holiday " + hdays[i].toString());
                   stats++;
                }
                else
                {
                   dList("Ignored holiday " + hdays[i].toString());
                   statf++;
                }
             }

             dStat("Loaded " + stats + " holidays, " + statf + " ignored");
          }
       }

       /**
        * Reads the maximum limits for simultaneous user sessions.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        *
        * @throws   ConfigurationException
        *           If maxAge parameter is out of bounds.
        */
       private void readInteractiveUserLimits(DirectoryService ds)
       throws ConfigurationException
       {
          Integer msiu = ds.getNodeInteger("/security/config/maxusers", "value");

          if (msiu != null)
          {
             if (msiu < -1)
             {
                throw new ConfigurationException("Invalid maximum simultaneous interactive users (maxusers) " +
                                                 msiu);
             }

             maxInteractiveUsers = msiu;
             dList("Loaded maximum simultaneous interactive users (maxusers) " + msiu);
          }
          else
          {
             maxInteractiveUsers = -1;
          }

          Integer mus = ds.getNodeInteger("/security/config/maxusersessions", "value");

          if (mus != null)
          {
             if (mus < -1)
             {
                throw new ConfigurationException("Invalid maximum simultaneous sessions per interactive user " +
                                                 "(maxusersessions) " + mus);
             }

             maxSessionsPerUser = mus;
             dList("Loaded maximum simultaneous sessions per interactive user (maxusersessions) " + mus);
          }
          else
          {
             maxSessionsPerUser = -1;
          }
       }

       /**
        * Reads the password aging control parameters.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        *
        * @throws   ConfigurationException
        *           If maxAge parameter is out of bounds.
        */
       private void readPasswordAging(DirectoryService ds)
       throws ConfigurationException
       {
          // reading maximum password age from directory
          Integer mpa = ds.getNodeInteger("/security/config/change/maxage", "value");

          if (mpa != null)
          {
             maxAge = mpa.intValue();
             if (maxAge < 0)
                throw new ConfigurationException("invalid password age");

             dList("Loaded maximum password age " + maxAge);
          }
          else
          {
             maxAge = 0;
          }

          // reading authorization hook class from directory
          pwInputHook = ds.getNodeString("/security/config/change/plugin",
                                         "value");
          if (pwInputHook == null)
          {
             if (maxAge > 0)
                throw new ConfigurationException("password input hook required");
          }

          if (maxAge > 0 || pwInputHook != null)
             dStat("Loaded password aging object");
       }

       /**
        * Reads the P2J directory and loads defined resource plugins.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        *
        * @param    preload
        *           array of plugin names to bypass reading the directory
        *
        * @throws   NoSuchMethodException
        *           Thrown by use of <code>ResourceRegistry</code>.
        */
       void readPlugins(DirectoryService ds, String[] preload)
       throws NoSuchMethodException
       {
          String[] plugins = null;

          if (preload == null)
          {
             String nodeId = "/security/config/resource-plugins";

             // check the node for existence and proper type
             if (ds.getNodeClass(nodeId) == null)
             {
                dWarn("No resource plugins defined in P2J directory");
                return;
             }

             if (!ds.getNodeClass(nodeId).equals("/meta/class/strings"))
             {
                dWarn("No resource plugins defined in P2J directory");
                return;
             }

             // read all plugin names
             plugins = ds.getNodeStrings(nodeId, "values");
             if (plugins == null || plugins.length == 0)
             {
                dWarn("No resource plugins defined in P2J directory");
                return;
             }
          }
          else
          {
             plugins = preload;
          }

          // register listed plugins
          ResourceRegistry reg[] = new ResourceRegistry[plugins.length];
          int rejects = 0;
          Class pluginClass = null;
          AbstractResource pluginInstance = null;
          resolver = new Resolver();
          Object library = null;

          int stats = 0;
          int statf = 0;

          // first pass
          for (int i = 0; i < plugins.length; i++)
          {
             reg[i] = null;

             // create an instance of the class
             try
             {
                pluginClass    = Class.forName(plugins[i]);
                pluginInstance = (AbstractResource)pluginClass.getDeclaredConstructor().newInstance();
                stats++;
             }
             catch (Exception e)
             {
                // something's wrong with the class
                rejects++;
                dErr("Failed to register plugin " + plugins[i] +
                                   ": " + e);
                statf++;
                continue;
             }

             // attach the instance to the Security Manager
             pluginInstance.attach(sm);

             // create the registry entry
             reg[i] = new ResourceRegistry(i, pluginInstance);

             // check for the resource type name duplication
             String thisName = reg[i].getTypeName();
             for (int j = 0; j < i - 1; j++)
             {
                if (reg[j] != null && reg[j].getTypeName().equals(thisName))
                {
                   dErr("Plugin " + plugins[i] + " exports resource type name "
                           + thisName + " already claimed by " + plugins[j]);
                   reg[i] = null;
                   rejects++;
                   statf++;
                   stats--;
                   break;
                }
             }
             if (reg[i] == null)
                continue;

             // register exported variables and functions
             library = pluginInstance.getLibrary();
             if (library != null)
             {
                resolver.registerLibrary(pluginInstance, thisName, library);
             }
          }

          // second pass - compaction of the array
          if (plugins.length == rejects)
          {
             dWarn("No valid resource plugins defined in P2J directory");
             return;
          }

          registry = new ResourceRegistry[plugins.length - rejects];

          int j = 0;
          for (int i = 0; i < plugins.length; i++)
          {
             if (reg[i] != null)
             {
                registry[j] = reg[i];
                pluginInstance = registry[j].getPlugin();
                pluginInstance.registeredAs(j);
                dList("Loaded resource plugin for <" + reg[i].getTypeName() +
                         "> as " + j);
                j++;
             }
          }

          dStat("Loaded " + stats + " resource plugins, " + statf + " failed");
       }

       /**
        * Reads the P2J directory and returns the custom extension plugin name.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        *
        * @param    nodeId
        *           where to look for the extension
        *
        * @return   extension name or <code>null</code>
        */
       private String readCustomExtension(DirectoryService ds, String nodeId)
       {
          // check the node for existence and proper type
          if (ds.getNodeClass(nodeId) == null)
          {
             return null;
          }

          if (!ds.getNodeClass(nodeId).equals("/meta/class/string"))
          {
             return null;
          }

          // read the extension name
          String ext = ds.getNodeString(nodeId, "value");
          if (ext == null)
          {
             return null;
          }

          ext = ext.trim();
          if (ext.length() == 0)
          {
             return null;
          }

          return ext;
       }

       /**
        * Reads the P2J directory and loads the defined ACLs.
        * <p>
        * ACLs may be defined under the shared branch of the directory, namely,
        * <code>/security/acl</code> and/or under the server's private branch
        * <code>/security/acls/<i>server-name </i></code>. If both branches
        * contain definitions, they will be logically combined.
        * <p>The basis for this combination is the ACL sequence numbers, which are
        * node names. All definitions are read from both the branches and then
        * processed in the ascending order of their sequence numbers.
        * If a sequence number is used in both shared and private branches, the
        * ACL definition from the private branch is used. This way the private
        * branch can simply add definitions to the combinated set or replace some
        * shared definitions.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        */
       private void readACLs(DirectoryService ds)
       throws ConfigurationException
       {
          String fullId = null;
          int ord = -1;

          // first, enumerate shared ACL groups by abstract resource name
          String pathShared = "/security/acl";
          String[] resTypesShared = ds.enumerateNodes(pathShared);

          // second, enumerate private ACL groups, if any
          String pathPriv = new String("/security/acls/" + serverId);
          String[] resTypesPriv = ds.enumerateNodes(pathPriv);

          // combine both arrays
          String resTypes[] = null;
          int numShared = resTypesShared == null ? 0 : resTypesShared.length;
          int numPriv = resTypesPriv   == null ? 0 : resTypesPriv.length;

          // create a union of two sets of names - supported resource types
          Set s = new HashSet();
          Set ssh = new HashSet();
          Set spr = new HashSet();
          int i = 0;

          for (i = 0; i < numShared; i++)
          {
             s.add(resTypesShared[i]);
             ssh.add(resTypesShared[i]);
          }

          for (i = 0; i < numPriv; i++)
          {
             s.add(resTypesPriv[i]);
             spr.add(resTypesPriv[i]);
          }

          int numAcls = s.size();
          if (numAcls == 0)
             dWarn("No ACL groups defined in the P2J directory");
          else
          {
             resTypes = new String[numAcls];

             Iterator iter = s.iterator();
             i = 0;
             while (iter.hasNext())
             {
                resTypes[i] = (String)iter.next();
                i++;
             }
          }

          // process ACLs by resource type
          for (i = 0; i < numAcls; i++)
          {
             // ACLs being built are collected in this list
             List acls = new LinkedList();

             // check if this resource type is registered
             ord = getRegistryOrdByTypeName(resTypes[i]);
             if (ord == -1)
             {
                dWarn("ACLs branch <" + resTypes[i] + "> is ignored");
                continue;
             }

             dList("ACLs for resource type <" + resTypes[i] + ">:");

             SortedMap rmap = new TreeMap();

             // enumerate all rights object for this resource type - shared
             if (ssh.contains(resTypes[i]))
             {
                addAclNodes(ds, pathShared + "/" + resTypes[i], rmap, ord);
             }

             // enumerate all rights object for this resource type - private
             if (spr.contains(resTypes[i]))
             {
                addAclNodes(ds, pathPriv + "/" + resTypes[i], rmap, ord);
             }

             // processing ordered sequence of ACLs
             Set rset = rmap.keySet();
             Iterator iter = rset.iterator();
             Rights rights = null;

             while (iter.hasNext())
             {
                String path = (String)rmap.get(iter.next());

                // instantiate the Rights object
                rights = createRights(ds, ord, path + "/rights");
                if (rights == null)
                {
                   throw new ConfigurationException("cannot create Rights for " +
                                                    path);
                }

                // read this ACL container and create the ACL
                createACL(ds, path, ord, rights, acls);
             }

             printACLs(acls);

             AccessControlList[] arrayACL = new AccessControlList[acls.size()];
             registry[ord].setACLs((AccessControlList[])acls.toArray(arrayACL));

             dStat("Loaded " + acls.size() + " ACLs for resource type <" +
                      resTypes[i] + ">");
          }
       }

       /**
        * Enumerates a directory branch and gathers all ACLs from there.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        *
        * @param    fullId
        *           directory branch
        *
        * @param    map
        *           storage for ACL paths
        *
        * @param    ord
        *           resource plugin ordinal number
        */
       private void addAclNodes(DirectoryService ds, String fullId, Map map,
                                int ord)
       throws ConfigurationException
       {
          String[] seqno = ds.enumerateNodes(fullId);

          // check if ACL conversion is due
          if (seqno.length > 0)
          {
             boolean migrated = migrateACLs(ds, fullId, seqno, ord);
             if (migrated)
             {
                seqno = ds.enumerateNodes(fullId);
             }
          }

          // sorting ACLs in the ascending order of seqno
          for (int j = 0; j < seqno.length; j++)
          {
             String rpath = fullId + "/" + seqno[j];
             int k = 0;

             try
             {
                k = Integer.parseInt(seqno[j]);
             }
             catch (NumberFormatException e)
             {
                dWarn("ACLs branch " + rpath + " is ignored");
                continue;
             }

             map.put(Integer.valueOf(k), rpath);
          }
       }

       /**
        * Checks migration status of the given ACL branch and migrates it fron
        * the old tree to the new flat representation when necessary.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        *
        * @param    path
        *           directory branch
        *
        * @param    nodes
        *           array of existing nodes in the branch
        *
        * @param    ord
        *           resource plugin ordinal number
        */
       boolean migrateACLs(DirectoryService ds, String path, String[] nodes,
                           int ord)
       throws ConfigurationException
       {
          // ACLs being built are collected in this list
          List acls = new LinkedList();
          SortedMap rmap = new TreeMap();

          // sorting child nodes in the ascending order of seqno
          // they are either rights (old style) or containers (new style)
          for (int j = 0; j < nodes.length; j++)
          {
             String rpath = path + "/" + nodes[j];
             int k = 0;

             if (ds.getNodeClass(rpath).equals("/meta/class/container"))
             {
                // found a container; assume new style and ignore everything else
                return false;
             }

             try
             {
                k = Integer.parseInt(nodes[j]);
             }
             catch (NumberFormatException e)
             {
                dWarn("ACLs branch " + rpath + " is ignored");
                continue;
             }

             rmap.put(Integer.valueOf(k), rpath);
          }

          // conversion is due
          if (!serverAcc.isMaster())
          {
             throw new ConfigurationException(
                       "ACL conversion requires running as master server");
          }

          dWarn("*** converting ACLs for " + path + " ***");

          // processing ordered sequence of rights
          Set rset = rmap.keySet();
          Iterator iter = rset.iterator();
          Rights rights = null;
          int step = 100;
          int seqn = step;
          boolean rc = false;

          while (iter.hasNext())
          {
             String rpath = (String)rmap.get(iter.next());
             dWarn("Converting ACLs branch " + rpath);
             acls.clear();

             // instantiate this Rights object
             rights = createRights(ds, ord, rpath);
             if (rights == null)
                throw new ConfigurationException("cannot create Rights for " +
                                                 rpath);

             // enumerate all bindings for this Rights
             String[] bseqno = ds.enumerateNodes(rpath);

             // sorting bindings in the ascending order of seqno
             SortedMap bmap = new TreeMap();

             for (int j = 0; j < bseqno.length; j++)
             {
                String bpath = rpath + "/" + bseqno[j];
                int l = 0;

                try
                {
                   l = Integer.parseInt(bseqno[j]);
                }
                catch (NumberFormatException e)
                {
                   dWarn("ACLs branch " + bpath + " is ignored");
                   continue;
                }

                bmap.put(Integer.valueOf(l), bseqno[j]);
             }

             // processing ordered sequence of bindings
             Set bset = bmap.keySet();
             Iterator biter = bset.iterator();
             String bisn = null;

             while (biter.hasNext())
             {
                bisn = (String)bmap.get(biter.next());
                String bpath = rpath + "/" + bisn;

                // read this binding directory object and create as many
                // separate ACLs as there are defined instances
                // ACLs are put into acls list.
                createOldACLs(ds, rpath, rights, bpath, acls);
             }

             // now read the list of ACLs and create new style ACL containers

             // open editing batch for the branch
             rc = ds.openBatch(path);
             if (!rc)
             {
                dErr("Can't lock directory branch '" + path +
                     "'. Migration aborted.");
                return false;
             };

             // creating new ACL containers
             Iterator ai = acls.iterator();
             newAcl:
             while (ai.hasNext())
             {
                AccessControlList acl = (AccessControlList)ai.next();

                // prepare resource instance name
                String instanceName = acl.getInstanceName();

                // prepare resource isntance name mode
                boolean instanceNameMode = acl.isExact();

                // prepare the reference to the original rights node
                String rightsRef = acl.getOrigin();

                // get all bindings for this resource instance
                List binds = acl.getBindings();

                // treat each binding separately
                Iterator bi = binds.iterator();
                while (bi.hasNext())
                {
                   Binding bind = (Binding)bi.next();

                   // get the array of subjects
                   int[] nsubs = bind.getSubjects();

                   // prepare the subjects as names
                   String[] subjects = new String[nsubs.length];
                   for (int i = 0; i < nsubs.length; i++)
                   {
                      if (nsubs[i] == -2)
                      {
                         subjects[i] = "all_others";
                      }
                      else
                      {
                         subjects[i] = getAccountByOrd(nsubs[i]).getSubjectId();
                      }
                   }

                   // create the container
                   String seq = String.format("%06d", seqn);
                   seqn += step;

                   String cid = path + '/' + seq;
                   rc = ds.addNode(cid, "container", null);
                   if (!rc)
                   {
                      dErr("can't add container " + cid);
                      break newAcl;
                   }

                   // copy the rights node
                   rc = ds.copyNode(rightsRef, cid + "/rights", false);
                   if (!rc)
                   {
                      dErr("can't copy rights " + rightsRef + " to " + cid);
                      break newAcl;
                   }

                   // create resource instance node
                   Attribute[] rdata = new Attribute[]
                   {
                      // resource instance name
                      new Attribute(ds.getClassNodeAttribute("resource",
                                                             "reference"),
                                    new Object[] {instanceName}),
                      // resource instance mode
                      new Attribute(ds.getClassNodeAttribute("resource",
                                                             "reftype"),
                                    new Object[] {Boolean.valueOf(instanceNameMode)}),
                   };
                   rc = ds.addNode(cid + "/resource-instance", "resource", rdata);
                   if (!rc)
                   {
                      dErr("can't add resource-instance to " + cid);
                      break newAcl;
                   }

                   // create subjects node
                   Attribute[] sdata = new Attribute[]
                   {
                      // subjects
                      new Attribute(ds.getClassNodeAttribute("strings", "values"),
                                    subjects),
                   };
                   rc = ds.addNode(cid + "/subjects", "strings", sdata);
                   if (!rc)
                   {
                      dErr("can't add subjects to " + cid);
                      break newAcl;
                   }
                }
             }

             // remove the old style ACL node
             if (rc)
             {
                rc = Utils.removeDirectoryNode(ds, rpath);
                if (!rc)
                {
                   dErr("can't remove the old style ACLs " + rpath);
                }
             }

             // close batch with commit or rollback and refresh
             boolean rcc = ds.closeBatch(rc, false);
             if (!rcc)
             {
                dErr("Error unlocking directory branch '" + path + "'.");
             }
             else
             {
                dList(String.format("*** converted %d ACLs ***",
                                    (seqn - step)/step));
             }
          }

          // indicate the migration was performed
          return rc;
       }

       /**
        * Reads the P2J directory and loads audit related definitions.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        */
       private void readAudit(DirectoryService ds)
       throws ConfigurationException
       {
          // the very first check is to see if auditing is enabled
          Boolean enabled = ds.getNodeBoolean("/security/audit/enabled", "value");
          if (enabled == null || !enabled.booleanValue())
          {
             // instantiate disabled audit object
             dWarn("Security audit log is disabled");
             audit = new Audit();
             return;
          }

          // the second check is to see if the audit log file is defined
          String logFile = ds.getNodeString("/security/audit/logfile", "value");
          if (logFile == null)
             throw new ConfigurationException(
                       "audit log file not defined while auditing is enabled");

          // read various audit definitions
          Integer logSizeC = ds.getNodeInteger("/security/audit/logsize",
                                               "value");
          int logSize = logSizeC == null ? 8096 : logSizeC.intValue();
          if (logSize < 0)
             throw new ConfigurationException(
                       "audit log size is invalid: " + logSize);

          Integer logCountC = ds.getNodeInteger("/security/audit/logcount",
                                                "value");
          int logCount = logCountC == null ? 3 : logCountC.intValue();
          if (logCount < 1)
             throw new ConfigurationException(
                       "audit log count is invalid: " + logCount);

          dList("Security audit log is <" + logFile + ">, " + logCount + ", " +
                   logSize + "K");

          int statd = 0;
          int stats = 0;
          int statr = 0;

          Boolean filterModeC = ds.getNodeBoolean("/security/audit/filterMode",
                                                  "value");
          boolean filterMode = filterModeC == null ?
                               false : filterModeC.booleanValue();

          Boolean decSuccessC = ds.getNodeBoolean("/security/audit/decisions",
                                                  "success");
          boolean decSuccess = decSuccessC == null ?
                               false : decSuccessC.booleanValue();
          Boolean decFailureC = ds.getNodeBoolean("/security/audit/decisions",
                                                  "failure");
          boolean decFailure = decFailureC == null ?
                               false : decFailureC.booleanValue();

          if (decSuccess)
             statd++;
          if (decFailure)
             statd++;
          dList("Audit filtering mode is " + (filterMode ? "AND" : "OR"));
          dList("Audit decisions: success: " + (decSuccess ? "YES " : "NO ") +
                   "failure: " + (decFailure ? "YES " : "NO "));

          // read subject IDs and convert them into numeric form
          String[] auditSubjs = ds.getNodeStrings("/security/audit/subjects",
                                                  "values");
          int numSubjs = auditSubjs == null ? 0 : auditSubjs.length;
          int ord = 0;
          int[] auditIds = null;
          if (numSubjs > 0)
             auditIds = new int[numSubjs];

          StringBuilder sb = new StringBuilder();

          for (int i = 0; i < numSubjs; i++)
          {
             ord = getOrdinalById(auditSubjs[i]);
             if (ord == -1)
                dWarn("Audit definition refers to unknown account "
                         + auditSubjs[i]);
             else
                sb.append("<" + auditSubjs[i] + ">, ");
             auditIds[i] = ord;
          }
          if (numSubjs > 0)
          {
             dList("Audit subjects " + new String(sb));
             stats = numSubjs;
          }

          // enumerate auditable resources
          String path = "/security/audit/resources";
          String[] res = ds.enumerateNodes(path);
          int rsize = res == null ? 0 : res.length;

          // process auditable resources
          String fullId = null;
          String resType = null;
          String[] instances = null;
          Integer[] reqRights = null;
          AuditTarget target = null;

          for (int i = 0; i < rsize; i++)
          {
             fullId = new String(path + "/" + res[i]);
             if (!ds.getNodeClass(fullId).equals("/meta/class/auditResource"))
                continue;

             // read resource type and identify the plugin
             resType = ds.getNodeString(fullId, "type");
             if (resType == null)
             {
                dWarn("cannot read resource type for " + fullId);
                continue;
             }

             ord = getRegistryOrdByTypeName(resType);
             if (ord == -1)
             {
                dWarn("cannot identify resource plugin for " + fullId + ", " +
                         resType);
                continue;
             }

             ResourceRegistry rr = getRegistryByOrd(ord);
             AbstractResource ar = rr.getPlugin();
             target = rr.getTarget();

             // read instances and requested rights
             instances = ds.getNodeStrings(fullId, "instances");
             reqRights = ds.getNodeIntegers(fullId, "requests");

             // create a registration list
             String[] reg = null;
             if (instances == null)
             {
                reg = new String[1];
                reg[0] = null;
             }
             else
             {
                reg = instances;
                statr += instances.length;
             }

             sb.setLength(0);

             // register audit target
             for (int j = 0; j < reg.length; j++)
             {
                if (reg[j] != null)
                   if (!ar.isInstanceNameValid(reg[j]))
                       {
                          dWarn("invalid resource instance name " + reg[j]);
                          continue;
                       }
                target.addTarget(reg[j], reqRights);
                sb.append("<" + reg[j] + ">, ");
             }

             sb.append("access modes ");
             if (reqRights == null)
                sb.append("all");
             else
                for (int j = 0; j < reqRights.length; j++)
                   sb.append(reqRights[j] + ", ");

             dList("Audit resource <" + resType + "> instances " + sb);
          }

         // instantiate and set up the audit object
         try
         {
            audit = new Audit(this, logFile, logSize, logCount);
         }
         catch (IOException ioe)
         {
            throw new ConfigurationException("audit log file cannot be created");
         }
         audit.setFilterMode(filterMode);
         audit.setAuditSuccess(decSuccess);
         audit.setAuditFailure(decFailure);
         audit.setAuditId(auditIds);

         dStat("Audit decisions: " + statd + ", subjects: " + stats +
                  ", resources " + statr);
       }

       /**
        * Verifies that the server ID is valid.
        * <p>
        * Server ID is valid if:
        * <ul>
        *   <li>it has a matching process account;
        *   <li>that account is for a server;
        *   <li>that account's alias points to a matching trust store certificate
        * </ul>
        *
        * @param    secure
        *           Specifies whether secure environment is enabled.
        *
        * @throws   ConfigurationException
        *           If the server ID is invalid.
        */
       private void verifyServerId(boolean secure)
       throws ConfigurationException
       {
          Account acc = getAccountById(serverId);
          if (acc == null)
             throw new ConfigurationException("server ID <" + serverId +
                                              "> has no account");

          if (acc.getAccountType() != Account.ACC_PROCESS)
             throw new ConfigurationException("wrong account type for server ID <"
                                              + serverId + ">");

          serverAcc = (ProcessAccount)acc;
          if (!serverAcc.isServer())
             throw new ConfigurationException("server ID <" + serverId +
                                              "> is for an application");

          serverAlias = serverAcc.getAlias();
          if (serverAlias == null)
             throw new ConfigurationException(
                       "server ID <" + serverId + "> has no certificate alias");
          if (secure)
          {
             try
             {
                java.security.cert.Certificate cert = trustStore.getCertificate(
                                                                 serverAlias);
                if (cert == null)
                   throw new ConfigurationException("server's alias <"
                             + serverAlias + "> has no certificate");
             }
             catch (KeyStoreException e)
             {
                throw new ConfigurationException("server's alias <" + serverAlias +
                                                 "> has no certificate");
             }
          }
       }

       /**
        * Verifies that the anonymous process account ID is valid.
        * <p>
        * Anonymous process account ID is valid if:
        * <ul>
        *   <li>it has a matching process account;
        *   <li>that account is for an application;
        *   <li>that account has no truststore alias;
        * </ul>
        *
        * @throws   ConfigurationException
        *           If the anonymous ID is invalid.
        */
       private void verifyAnonymousId()
       throws ConfigurationException
       {
          if (anonymId == null)
             return;

          Account acc = getAccountById(anonymId);
          if (acc == null)
             throw new ConfigurationException("anonymous ID <" + anonymId +
                                              "> has no account");

          if (acc.getAccountType() != Account.ACC_PROCESS)
             throw new ConfigurationException(
                       "wrong account type for anonymous ID <" + anonymId + ">");

          ProcessAccount proc = (ProcessAccount)acc;
          if (proc.isServer())
             throw new ConfigurationException("anonymous ID <" + anonymId +
                                              "> is for a server");

          if (proc.getAlias() != null)
             throw new ConfigurationException(
                       "anonymous ID <" + anonymId + "> has a certificate alias");
       }

       /**
        * Creates an instance of Rights based on information in the P2J directory.
        *
        * @param    ds
        *           Instance of <code>DirectoryService</code> from which to read.
        * @param    ord
        *           Ordinal number of the resource type.
        * @param    path
        *           P2J directory path to the instance of <code>Rights</code>.
        *
        * @return   New instance of <code>Rights</code> or <code>null</code>.
        */
       Rights createRights(DirectoryService ds, int ord, String path)
       {
          // locate the plugin responsible for this resource type
          AbstractResource plugin = getRegistryByOrd(ord).getPlugin();
          if (plugin == null)
             return null;

          // read all attributes of this instance of Rights
          Attribute[] attr = ds.getNodeAttributes(path);
          if (attr == null)
             return null;

          // get fields description
          Description[] descr = getRegistryByOrd(ord).getDescription();
          if (descr == null)
             return null;

          // instantiate field objects
          Object[] fields = new Object[descr.length];
          String label = null;

          for (int i = 0; i < descr.length; i++)
          {
             label = descr[i].getLabel();
             int j = 0;

             // find a matching attribute
             for (j = 0; j < attr.length; j++)
             {
                if (attr[j].getName().equals(label))
                   break;
             }
             if (j < attr.length)
                fields[i] = attr[j].getValue(0);
             else
             {
                if (!descr[i].isOptional())
                   return null;
                else
                   fields[i] = null;
             }
          }

          // create Rights instance from fields
          if (!plugin.isRightsSetValid(fields))
          {
             return null;
          }

          return plugin.getRightsInstance(fields);
       }

       /**
        * Reads the P2J directory and loads ACLs from the specified binding
        * directory object into a list.
        * <p>
        * A binding object is made of:
        * <ul>
        *   <li>a list of one or more subject IDs and, possibly, all_others
        *       keyword;
        *   <li>a list of resource instance names;
        *   <li>a list of resource instance reference types: <code>true</code>
        *       meaning exact name and <code>false</code> meaning regexp match
        * </ul>
        * The number of values for instance names and for reference types must be
        * the same. That does not depend on the number of subject IDs.
        * <p>
        * The result of this method is a number of <code>AccessControlList</code>
        * entries added to the list "list", one entry per instance name. These
        * <code>AccessControlList</code>s will have exactly one
        * <code>Binding</code> object put into the embedded list of bindings:
        * {parameter rights, list of subjects from oid}.
        *
        * @param ds
        *           Instance of <code>DirectoryService</code> from which to read.
        * @param roid
        *           Rights directory object ID
        * @param rights
        *           Object to be kept in triplets.
        * @param boid
        *           Binding directory object ID to read.
        * @param list
        *           Place to put the instances of ACLs.
        */
       private void createOldACLs(DirectoryService ds, String roid, Rights rights,
                                  String boid, List list)
       throws ConfigurationException
       {
          // read the set of subjects from this parent object (strings)
          String[] subjects   = ds.getNodeStrings(boid, "values");
          if (subjects == null)
             throw new ConfigurationException("error 1 reading directory object "
                                              + boid);

          // enumerate all bindings for this parent object
          String[] bseqno = ds.enumerateNodes(boid);
          if (bseqno == null)
             throw new ConfigurationException("error 2 reading directory object "
                                              + boid);

          // sorting bindings in the ascending order of seqno
          SortedMap bmap = new TreeMap();

          for (int j = 0; j < bseqno.length; j++)
          {
             String bpath = boid + "/" + bseqno[j];
             int l = 0;

             try
             {
                l = Integer.parseInt(bseqno[j]);
             }
             catch (NumberFormatException e)
             {
                dWarn("ACLs branch " + bpath + " is ignored");
                continue;
             }

             bmap.put(Integer.valueOf(l), bseqno[j]);
          }

          // processing ordered sequence of bindings
          Set bset = bmap.keySet();
          if (bset.size() == 0)
             throw new ConfigurationException("error 3 reading directory object "
                                              + boid);
          Iterator biter = bset.iterator();
          String bisn = null;
          String[] references = new String[bset.size()];
          Boolean[] reftypes  = new Boolean[bset.size()];
          String[] refpaths   = new String[bset.size()];

          int i = 0;
          while (biter.hasNext())
          {
             bisn = (String)bmap.get(biter.next());
             String bpath = boid + "/" + bisn;
             refpaths[i]  = bpath;

             // read this binding directory object
             references[i] = ds.getNodeString(bpath, "reference");
             if (references[i] == null)
                throw new ConfigurationException(
                          "error 4 reading directory object " + bpath);
             reftypes[i]   = ds.getNodeBoolean(bpath, "reftype");
             if (reftypes[i] == null)
                throw new ConfigurationException(
                          "error 5 reading directory object " + bpath);
             i++;
          }

          // resolve subject IDs into ordinals
          int[] ordinals = new int[subjects.length];

          for (i = 0; i < subjects.length; i++)
          {
             ordinals[i] = getOrdinalById(subjects[i]);
             if (ordinals[i] == -1)
             {
                if (subjects[i].compareToIgnoreCase("all_others") == 0)
                   ordinals[i] = -2;
                else
                   throw new ConfigurationException("directory object " + boid +
                             " refers to non-existing account " + subjects[i]);
             }
          }

          // can't share a single binding list between all resource instances
          // have to create an instance for every AccessControlList
          // but can share the single binding

          Binding bind = new Binding(rights, ordinals);
          List bindList = null;

          // create ACLs, one per resource instance
          AccessControlList acl = null;

          for (i = 0; i < references.length; i++)
          {
             bindList = new LinkedList();
             bindList.add(bind);

             // use roid (rights object id) as a reference to the original node
             acl = new AccessControlList(roid,
                                         references[i],
                                         reftypes[i].booleanValue(),
                                         bindList);
             list.add(acl);
          }
       }

       /**
        * Reads the P2J directory and loads ACLs from the specified
        * directory object into a list.
        * <p>
        * <p>
        * The result of this method is a number of <code>AccessControlList</code>
        * entries added to the list "list", one entry per instance name. These
        * <code>AccessControlList</code>s will have exactly one
        * <code>Binding</code> object put into the embedded list of bindings:
        * {parameter rights, list of subjects from oid}.
        *
        * @param ds
        *           Instance of <code>DirectoryService</code> from which to read.
        * @param path
        *           directory object ID of the container of this ACL
        * @param rights
        *           Object to be kept in triplets.
        * @param list
        *           Place to put the instances of ACLs.
        */
       private void createACL(DirectoryService ds, String path, int ord,
                              Rights rights, List list)
       throws ConfigurationException
       {
          // locate the plugin responsible for this resource type
          AbstractResource plugin = getRegistryByOrd(ord).getPlugin();

          // read the set of subjects from this ACL container (strings)
          String[] subjects   = ds.getNodeStrings(path + "/subjects", "values");
          if (subjects == null)
          {
             throw new ConfigurationException("no subjects list in ACL " + path);
          }

          // resolve subject IDs into ordinals
          int[] ordinals = new int[subjects.length];

          for (int i = 0; i < subjects.length; i++)
          {
             ordinals[i] = getOrdinalById(subjects[i]);
             if (ordinals[i] == -1)
             {
                if (subjects[i].compareToIgnoreCase("all_others") == 0)
                   ordinals[i] = -2;
                else
                   throw new ConfigurationException(
                             "directory object " +  path + "/subjects" +
                             " refers to non-existing account " + subjects[i]);
             }
          }

          // read the resource instance information
          String bpath = path + "/resource-instance";

          String reference = ds.getNodeString(bpath, "reference");
          if (reference == null)
          {
             throw new ConfigurationException("no resource name in ACL " + path);
          }

          Boolean reftype = ds.getNodeBoolean(bpath, "reftype");
          if (reftype == null)
          {
             throw new ConfigurationException("no match type in ACL " + path);
          }

          // for exact match instances, verify the instance name
          if (reftype.booleanValue())
          {
             if (!plugin.isInstanceNameValid(reference))
             {
                throw new ConfigurationException("directory object " +  bpath +
                          " refers to invalid resource instance name <" +
                          reference + ">");
             }
          }

          // create the ACL
          Binding bind = new Binding(rights, ordinals);
          List bindList = new LinkedList();
          bindList.add(bind);

          AccessControlList acl = new AccessControlList(path,
                                                        reference,
                                                        reftype.booleanValue(),
                                                        bindList);
          list.add(acl);
       }

       /**
        * Prints all ACLs from a list.
        *
        * @param    list
        *           List of ACLs.
        */
       private void printACLs(List list)
       {
          AccessControlList acl = null;
          int[] ids = null;
          List bindList = null;
          String reforig = null;
          Binding bind = null;
          Iterator iter = list.iterator();

          StringBuilder sb = new StringBuilder();

          while (iter.hasNext())
          {
             acl = (AccessControlList)iter.next();

             reforig = acl.getOrigin();
             dTrace("  origin " + reforig);

             sb.setLength(0);
             sb.append("   instance name <" + acl.getInstanceName() + "> ");
             if (acl.isExact())
                sb.append("(exact)");
             else
                sb.append("(regexp)");
             dList(sb.toString());

             bindList = acl.getBindings();
             Iterator biter = bindList.iterator();

             while (biter.hasNext())
             {
                bind = (Binding)biter.next();
                dList("      rights " + bind.getRights());
                ids = bind.getSubjects();

                sb.setLength(0);
                sb.append("      subjects ");
                for (int i = 0; i < ids.length; i++)
                {
                   if (ids[i] == -2)
                      sb.append("*all_others* ");
                   else
                      if (ids[i] == -1)
                         sb.append("*undefined* ");
                      else
                         sb.append(getAccountByOrd(ids[i]).getSubjectId() + " ");
                }
                dList(sb.toString());
             }
          }
       }

       /**
        * Logs error messages.
        *
        * @param  message
        *         text to be displayed
        */
       public void dErr(String message)
       {
          if (logMode == LM_ADMIN)
          {
             AdminServerImpl.message(new String[] {message});
          }
          if (logMode == LM_DUAL)
          {
             msgBuf.add(new String[] {message});
          }
          if (logMode == LM_CONSOLE || logMode == LM_DUAL)
          {
             LOG.severe(message);
          }
       }

       /**
        * Logs warnings.
        *
        * @param  message
        *         text to be displayed
        */
       public void dWarn(String message)
       {
          if (logMode == LM_ADMIN)
          {
             AdminServerImpl.message(new String[] {message});
          }
          if (logMode == LM_DUAL)
          {
             msgBuf.add(new String[] {message});
          }
          if (logMode == LM_CONSOLE || logMode == LM_DUAL)
          {
             LOG.warning(message);
          }
       }

       /**
        * Logs statistics.
        *
        * @param  message
        *         text to be displayed
        */
       public void dStat(String message)
       {
          if (logMode == LM_ADMIN)
          {
             AdminServerImpl.message(new String[] {message});
          }
          if (logMode == LM_DUAL)
          {
             msgBuf.add(new String[] {message});
          }
          if (logMode == LM_CONSOLE || logMode == LM_DUAL)
          {
             LOG.info(message);
          }
       }

       /**
        * Logs detailed lists.
        *
        * @param  message
        *         text to be displayed
        */
       public void dList(String message)
       {
          if (logMode == LM_ADMIN)
          {
             AdminServerImpl.message(new String[] {message});
          }
          if (logMode == LM_DUAL)
          {
             msgBuf.add(new String[] {message});
          }
          if (logMode == LM_CONSOLE || logMode == LM_DUAL)
          {
             LOG.fine(message);
          }
       }

       /**
        * Logs detailed traces.
        *
        * @param  message
        *         text to be displayed
        */
       public void dTrace(String message)
       {
          if (logMode == LM_ADMIN)
          {
             AdminServerImpl.message(new String[] {message});
          }
          if (logMode == LM_DUAL)
          {
             msgBuf.add(new String[] {message});
          }
          if (logMode == LM_CONSOLE || logMode == LM_DUAL)
          {
             LOG.finer(message);
          }
       }

       /**
        * Logs detailed data, possibly including sensitive information.
        *
        * @param  message
        *         text to be displayed
        */
       public void dData(String message)
       {
          if (logMode == LM_ADMIN)
          {
             AdminServerImpl.message(new String[] {message});
          }
          if (logMode == LM_DUAL)
          {
             msgBuf.add(new String[] {message});
          }
          if (logMode == LM_CONSOLE || logMode == LM_DUAL)
          {
             LOG.finest(message);
          }
       }

       /**
        * Find and update user account stored in cache.
        *
        * @param   user
        *          User account parameters.
        *
        * @return  <code>true</code> if account is found and updated.
        *          <code>false</code> if account not found.
        */
       public boolean updateCachedUserAccount(UserDef user)
       {
          boolean result = false;

          // iterate over cached accounts.
          for (Account acc : accs)
          {
             // search for account.
             if (acc instanceof UserAccount && user.subjectId.equals(acc.getSubjectId()))
             {
                // if found update acccount.
                acc.setEnabled(user.enabled);
                ((UserAccount) acc).setPassword(user.password, new DateValue(user.dateTime),
                         new TimeValue(user.dateTime));
                result = true;
             }
          }

          return result;
       }
    }