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;
   }
}