SecurityAdmin.java

/*
** Module   : SecurityAdmin.java
** Abstract : Collection of security administration workers and session state
**
** Copyright (c) 2009-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- --------------------------------Description-------------------------------------
** 001 NVS 20090325   @41682 Created the initial version since security
**                           workers are not visible to the admin package
** 002 NVS 20090429   @42083 Added isMaster() method.
**                           Implemented listProcesses(), getProcess(),
**                           setProcess(), addProcess(), deleteProcess(),
** 003 NVS 20090505   @42096 Implemented certificate related methods.
** 004 NVS 20090511   @42139 Implemented user account related methods.
** 005 NVS 20090518   @42243 Implemented moveCert() method.
** 006 NVS 20090519   @42334 Implemented searchCert() method. Improved
**                           checkCertificate() worker to block the wrong
**                           domain requests (adding peer cert to ca and
**                           ca cert to peer domains).
** 007 NVS 20090520   @42418 Added listUserGroups() API implementation.
** 008 NVS 20090525   @42451 Added temporary code to support custom account
**                           extensions. To be reimplemented.
** 009 NVS 20090528   @42479 Implemented interfacing with custom account
**                           extension plugin permanently.
** 010 CA  20090603   @42556 Avoid possible NPE's in getUser and getProcess.
** 011 CA  20090604   @42558 If adding or saving the extension fails with an
**                           exception, then rollback and perform a fast exit.
** 012 NVS 20090604   @42605 cloneUser() optionally replaces the password.
** 013 NVS 20090608   @42612 Full support for enabled/disabled account state. 
** 014 NVS 20090609   @42618 Automatically create password aging timestamp if
**                           user account is password protected and the
**                           password is provided but not the timestamp.
** 015 NVS 20090609   @42622 Added consReloadDirectory() implementation.
** 016 CA  20090615   @42693 Added getAdminDef(). Now also populates a map 
**                           with the description for each resource name.
** 017 GES 20090610   @42761 Added getSessionList().
** 018 GES 20090619   @42783 Added killSession().
** 019 NVS 20090625   @42958 Implemented cloning ACLs and proper removal of
**                           ACLs on account removals.
** 020 NVS 20090626   @42967 Implemented the first group of ACL related APIs -
**                           listAclInstances() and the like
** 021 NVS 20090701   @43006 Implemented the ACL getters group of the ACL 
**                           related APIs.
** 022 NVS 20090703   @43045 Implemented the addAcl() method.
** 023 NVS 20090703   @43050 Implemented the setAcl() method.
** 024 NVS 20090706   @43067 Implemented APIs moveAcl(), deleteAcl(),
**                           cloneAcl() and resequenceAcl().
** 025 NVS 20090707   @43082 Rights editor names are passed to the admin client
** 026 NVS 20090717   @43231 Fixed ALL_OTHERS subject handling in ACLs.
** 027 NVS 20090723   @43367 Added protection against various directory issues.
** 028 NVS 20090814   @43619 Added a command line, offline  ACL editing utility
**                           modifySubjectsInACLs() which can conditionally
**                           select ACLs for editing by a subject ID and apply
**                           specified deletions and/or additions of other
**                           subject IDs.
** 029 NVS 20090824   @43728 Extended the set of command line, offline directory
**                           utilities with these new utilities:
**                           - migrate - migrates an ACL section style
**                           - johndoe - validates accounts referenced by ACLs
**                           - resource - validates resources referenced by ACLs
**                           - encode - encodes passwords stored in another
**                           attribute as a plain text.
** 039 NVS 20090825   @43738 Fixed the recognition of all_others in johndoe
**                           utility.
** 040 NVS 20090826   @43773 Javadoc fix
** 041 NVS 20090827   @43778 Detailed messages from the security cache creation
**                           are logged in standalone utility environment.
** 042 NVS 20090910   @43858 Fixed sorting of ACLs in resequenceAcls() method.
** 043 NVS 20090918   @43953 Tells the transaction manager the session runs
**                           without the client side UI by setting the headless
**                           token in the session context.
** 044 NVS 20090924   @44024 Protected all account extension plugin calls with
**                           try/catch throwable to guarantee normal return
**                           from the admin APIs, thus avoiding deadlocks due
**                           non released open batched etc.
** 045 NVS 20090928   @44049 Fixed NPE in listGroupUsers() method. Changed
**                           closeBatch() related error message to a more
**                           informative one.
** 046 NVS 20090929   @44054 Fixed cloneUser() and cloneProcess() trying to 
**                           clone ACLs even when the batch for the account
**                           creation failed.
** 047 NVS 20090930   @44058 Added API protection against null parameters to
**                           multiple workers.
** 048 NVS 20091007   @44108 Implemented granular access rights checks for all
**                           user account related APIs.
** 049 SIY 20091027   @44261 Replaced DirectoryServices.refresh() with 
**                           DirectoryServices.atomicRefresh().
** 050 NVS 20091109   @44324 Fixed a NPE in getProfile() which was possible if
**                           the AdminResource plugin was not installed.
** 051 SIY 20091116   @44334 Refactoring of things related to TaggedName. 
**                           Minor cleanups.
** 052 SIY 20091216   @44512 Added support for "refresh" node.
** 053 SIY 20100419   @44824 Implemented automatic adding of function into
**                           directory if it does not exists.
** 054 SVL 20100729          Update auth mode while updating user account
**                           definitions.
** 055 SVL 20100916          Added support for group.mode, group.authPlugin
**                           and user.authPlugin fields.
** 056 SVL 20100920          Added bulkAuthModeUpdate function. Some
**                           refactoring was performed.
** 057 GES 20101118          Eliminated use of dead bootstrap config value for
**                           specification of the directory schema filename.
** 058 CA  20111209          Added method to list active sessions which have
**                           invoked APIs in the given jar or interface.
** 059 OM  20121002          Fallback to SystemResource sr if AdminResource ar 
**                           is null for checking admin access in getAdminDef()
** 060 CS  20121121          Removed client specific references.
** 061 ECF 20150828          Enable Java 8.
** 062 EVL 20160224          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 062 IAS 20160408          Use synthetic key as context key.
** 063 SBI 20170225          Moved administration profile type, access control list type and
**                           record lock type to their own modules respectively, removed DataValue
**                           and TimeValue fields out of UserDef.
** 064 GES 20210917          Minor code cleanup.
**     TJD 20220504          Java 11 compatibility minor changes
** 065 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 006 GBB 20230825          OS users methods added.
**                           SecurityManager context & session methods calls updated.
** 067 GBB 20240215          Handling RestrictedUseException in killSession().
** 068 TJD 20240123          Java 17 compatibility updates
** 069 SBI 20240307          Added missed resource logging for checkResourceType(String).
** 070 RNC 20240109          Added the getSessionById() method.
*/

/*
** 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.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateEncodingException;
import java.io.*;
import java.util.*;
import java.lang.reflect.*;

import com.goldencode.p2j.admin.server.reports.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.directory.Base64;
import com.goldencode.p2j.admin.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.*;

/**
 * This is a collection of workers that have to be in the security package
 * but used from the admin package. This class also maintains the session
 * related security cache which reflects the current state of the system being
 * edited in the persistent storage. 
 */
public class SecurityAdmin
implements SecurityConstants
{
   /** certificate encoding I/O chunk (as in *.PEM files) */
   private static final int SPLIT_CHUNK = 64;
   
   /** certificate encoding start mark */
   private static final String CERT_BEG = "-----BEGIN CERTIFICATE-----";

   /** certificate encoding end mark */
   private static final String CERT_END = "-----END CERTIFICATE-----";
   
   /** list of admin resource instance names maing up a profile */
   private static final String[] prof =
   {
      "/console",
      "/accounts",
      "/access_control",
      "/extension",
      "/runtime",
      "/configuration",
      "/target",
   };
   
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(SecurityAdmin.class);
   
   /** instance of security manager */
   private SecurityManager sm = null;

   /** instance of system resource plugin */
   private SystemResource sr = null;

   /** instance of admin resource plugin */
   private AdminResource ar = null;

   /** instance of current offline security cache */
   private SecurityCache sc = null;
   
   /** instance of directory service */
   private DirectoryService ds = null;
   
   /** instance of account extension plugin */
   private AdminAccountExtension ae = null;
   
   /** maps ACL instance name into map of resource types */
   private Map<String, Map<String, Map<Integer, String>>> mapInst = new
           HashMap<String, Map<String, Map<Integer, String>>>(3, (float)0.9);
                       
   /** current map from mapInst */
   private Map<String, Map<Integer, String>> mapTypes = null;

   /** current map from mapTypes */
   private Map<Integer, String> mapId = null;
   
   /**
    * Constructor.
    * 
    * @param    ds
    *           instance of DirectoryService to use for directory access
    */
   public SecurityAdmin(DirectoryService ds)
   {
      // store frequently used references
      sm = SecurityManager.getInstance();
      sr = (SystemResource)sm.getPluginInstance("system");
      ar = (AdminResource)sm.getPluginInstance("admin");
      this.ds = ds;
      ae = getExtension();
      
      // set the context to the headless mode to suppress UI
      sm.contextSm.addToken(ContextKey.HEADLESS_KEY, Boolean.TRUE);
      
      // get the offline security cache for this session (serial == -1)
      currentCache();
   }

   /**
    * Checks the rights of the caller to perform admin operations.
    * 
    * @return   <code>true</code> if the caller has sufficient rights
    */
   public boolean adminAccess()
   {
      // check the access rights
      return checkPermission();
   }

   /**
    * Tells whether the server can modify the directory. Only the master
    * server can. The others have read-only access to the directory.
    * 
    * @return   <code>true</code> if it can
    */
   public boolean isMaster()
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }

      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // get the server account
      ProcessAccount srv = sc.getServerAccount();
      return srv.isMaster();
   }
   
   /**
    * Check if there user can perform refresh.
    * 
    * @return   <code>true</code> if current user has enough rights to perform
    *           security cache refresh.
    */
   public boolean canRefresh()
   {
      // check the access rights
      if (!checkPermission("/target/refresh", 
                           AdminRights.ADLU_WRITE_ACCESS))
      {
         return false;
      }

      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      return true;
   }
   
   /**
    * Get the list of all active sessions which have executed API(s)
    * associated to this jar.
    *
    * @param   jar
    *          The name of the target jar file.
    * @param   iface
    *          The interface for which we need the sessions; 
    *          if <code>null</code>, search through the entire jar.
    *
    * @return  the list of all active sessions which have executed API(s)
    *          associated to this jar.
    */
   public SessionInfo[] getActiveSessions(String jar, String iface)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }

      return sm.sessionSm.getActiveSessions(jar, iface);
   }

   /**
    * Terminate all active sessions which have invoked the given APIs in the
    * given jar. If the jar name is <code>null</code>, terminate all active
    * tracked sessions.
    * This method guarantees that all session termination listeners have been
    * executed.
    *
    * @param jar   The jar name; if <code>null</code>, terminate all active
    *              tracked sessions.
    * @param iface The interface for which we need to terminate the sessions;
    *              if <code>null</code>, terminate all sessions for the given
    *              jar.
    *
    * @return     <code>true</code> if operation was successful,
    *             <code>false</code> if you do not have admin rights.
    */
   public boolean terminateSessions(String jar, String iface)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }

      sm.sessionSm.terminateSessions(jar, iface);
      return true;
   }

   /**
    * Returns a tagged array of defined group names. Tags are group
    * descriptions.
    * 
    * @return   All defined group accounts or <code>null</code>
    */
   public TaggedName[] listGroups()
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }

      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // get the list of accounts
      String[] gl = sc.getAllAccountIds(Account.ACC_GROUP);
      if (gl == null)
      {
         return null;
      }

      // create tagged list
      TaggedName[] tg = new TaggedName[gl.length];
      for (int i = 0; i < gl.length; i++)
      {
         tg[i] = new TaggedName(new String(gl[i]), 
                                new String(((GroupAccount)sc.getAccountById(gl[i])).
                                                              getDescription()));
      }
      
      return tg;
   }
   
   /**
    * Returns the group definition for the specified group name.
    * 
    * @param    name
    *           Group name.
    * 
    * @return   group definition or <code>null</code>
    */
   public GroupDef getGroup(String name)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // sanity check
      if (name == null)
      {
         message("Account name can't be omitted."); 
         return null;
      }
      
      // get the group account
      Account acc = sc.getAccountById(name);
      if (acc == null)
      {
         // no such account
         message("There is no account named '" + name + "'."); 
         return null;
      }
      
      if (acc.getAccountType() != Account.ACC_GROUP)
      {
         // not a group account
         message("Account '" + name + "' is not a group."); 
         return null;
      }
      
      // return the group definition
      GroupDef gd = new GroupDef();
      GroupAccount ga = (GroupAccount) acc;
      gd.subjectId = new String(ga.getSubjectId());
      gd.description = new String(ga.getDescription());
      gd.mode = ga.getAuthMode();
      gd.authPlugin = ga.getAuthPlugin() != null ?
                      new String(ga.getAuthPlugin()) :
                      null;
      
      return gd;
   }

   /**
    * Enumerates the users of a named group.
    * 
    * @param    name
    *           group name
    * 
    * @return   array of user names, that may be empty, or <code>null</code> if
    *           there is no such group
    */
   @SuppressWarnings("null")
   public TaggedName[] listGroupUsers(String name)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // sanity check
      if (name == null)
      {
         message("Account name can't be omitted."); 
         return null;
      }

      // get and check the group account's ordinal number
      int go = sc.getOrdinalById(name);
      if (go == -1)
      {
         // no such account
         message("There is no account named '" + name + "'."); 
         return null;
      }
      
      Account acc = sc.getAccountByOrd(go);
      if (acc.getAccountType() != Account.ACC_GROUP)
      {
         // not a group account
         message("Account '" + name + "' is not a group."); 
         return null;
      }
      
      // scan all users and select those ones who belong to the group
      String[] users = sc. getAllAccountIds(Account.ACC_USER);
      if (users.length == 0)
      {
         // no defined users
         return new TaggedName[] {};
      }
      
      List<TaggedName> lu = new LinkedList<TaggedName>();
      for (int i = 0; i < users.length; i++)
      {
         UserAccount ua = (UserAccount)sc.getAccountById(users[i]);
         int[] groups = ua.getGroups();
         int gsn = groups == null ? 0 : groups.length;
         
         for (int j = 0; j < gsn; j++)
         {
            if (groups[j] == go)
            {
               TaggedName tu = new TaggedName(new String(users[i]), 
                                              new String(ua.getPerson()));
               lu.add(tu);
            }
         }
      }
      
      return lu.toArray(new TaggedName[0]);
   }

   /**
    * Changes the group definition for the named group.
    * 
    * @param    group
    *           group definition
    * 
    * @return   <code>true</code> if successfully changed the group
    */
   public boolean setGroup(GroupDef group)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // get and check the group account's ordinal number
      if (group.subjectId == null)
      {
         message("Account name can't be omitted."); 
         return false;
      }
      
      int go = sc.getOrdinalById(group.subjectId);
      if (go == -1)
      {
         // no such account
         message("There is no account named '" + group.subjectId + "'."); 
         return false;
      }
      
      Account acc = sc.getAccountByOrd(go);
      if (acc.getAccountType() != Account.ACC_GROUP)
      {
         // not a group account
         message("Account '" + group.subjectId + "' is not a group."); 
         return false;
      }

      // validate auth mode
      if (group.mode < AUTH_MODE_LOWEST || group.mode > AUTH_MODE_HIGHEST) 
      {
         // invalid data
         message("Authentication mode is invalid.");
         return false;
      }

      // open editing batch for the branch
      boolean rc = ds.openBatch("/security/accounts/groups");
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build node ID
      String node = new String("/security/accounts/groups/" + 
                               group.subjectId);
      
      // sync the description attribute
      rc = syncString(node, "description", group.description);

      // sync auth settings
      if (rc)
      {
         rc = syncAuthSettings(node, group.mode, group.authPlugin);
      }
      
      if (!rc)
      {
         message("Directory modification failed for " + node);
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Adds new group account definition.
    * 
    * @param    group
    *           group definition for a new account
    * 
    * @return   <code>true</code> if successfully added the group
    */
   public boolean addGroup(GroupDef group)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // get and check the existing accounts
      if (group.subjectId == null)
      {
         message("Account name can't be omitted."); 
         return false;
      }
      
      int go = sc.getOrdinalById(group.subjectId);
      if (go != -1)
      {
         // account exists
         message("Name '" + group.subjectId + 
                 "' duplicates existing account name.");
         return false;
      }
      
      // open editing batch for the branch
      boolean rc = ds.openBatch("/security/accounts/groups");
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build node ID
      String node = new String("/security/accounts/groups/" + group.subjectId);
      
      // create group
      rc = createGroupNode(node, group.description, group.mode,
                           group.authPlugin);
      if (!rc)
      {
         message("Directory modification failed for " + node);
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false);
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Deletes a group account definition.
    * The account is deleted. All references to this group are deleted from
    * the user accounts and from ACLs.
    * <p>
    * The directory modification is done in multiple separate transactions:
    * <ul>
    *   <li>unassigning users from the group - 
    *        <code>/security/accounts/users</code>
    *   <li>removing the group from the ACLs -
    *        <code>/security/acl</code> and <code>/security/acls</code>
    *   <li>removing the group account -
    *        <code>/security/accounts/groups</code>
    * </ul>
    * This approach may leave the directory partially modified.
    *
    * @param    name
    *           group name for the group being deleted
    *
    * @return   <code>true</code> if successfully deleted the group
    */
   public boolean deleteGroup(String name)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // get the existing users of the group
      TaggedName[] tusers = listGroupUsers(name);
      if (tusers == null)
      {
         // no group etc
         return false;
      }
      
      String[] users = new String[tusers.length];
      for (int i = 0; i < tusers.length; i++)
      {
         users[i] = tusers[i].getName();
      }
      
      // unassign users from the group
      if (!changeGroupUsersWorker(name, new String[] {}, users))
      {
         // errors during unassign
         return false;
      }
      
      // remove the group from ACLs
      if (!removeSubjectFromACLs(name))
      {
         // errors during ACLs modification
         return false;
      }
      
      // open editing batch for the branch
      boolean rc = ds.openBatch("/security/accounts/groups");
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // remove the group node
      rc = ds.deleteNode("/security/accounts/groups/" + name);
      if (!rc)
      {
         message("Directory modification failed for " + 
                 "/security/accounts/groups/" + name);
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false);
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Clones an existing group account into a new one.
    * Users of the source group become also the users of the clone.
    * ACLs are modified to give the source group's rights to the clone, if
    * this option is requested.
    *
    * @param    nameFrom
    *           group name for the source group
    *
    * @param    nameTo
    *           group name for the clone
    *
    * @param    acls
    *           <code>true</code> tells to clone ACLs as well
    *
    * @return   <code>true</code> if successfully cloned the group
    */
   public boolean cloneGroup(String nameFrom, String nameTo, boolean acls)
   {
      // get the source group
      GroupDef sg = getGroup(nameFrom);
      
      if (sg == null)
      {
         // no source
         return false;
      }
      
      // add the target group
      GroupDef tg = new GroupDef();
      tg.subjectId = nameTo;
      tg.description = sg.description;
      tg.mode = sg.mode;
      tg.authPlugin = sg.authPlugin;
      
      boolean rc = addGroup(tg);
      if (!rc)
      {
         // problem creating group
         return false;
      }
      
      // get the existing users of the source group
      TaggedName[] tusers = listGroupUsers(nameFrom);
      if (tusers == null)
      {
         // inconsistency found
         message("Directory inconsistency found when listing users of group '"
                 + nameFrom + "'.");
         return false;
      }
      
      String[] users = new String[tusers.length];
      for (int i = 0; i < tusers.length; i++)
      {
         users[i] = tusers[i].getName();
      }
      
      // assign the users to the target group
      rc = changeGroupUsersWorker(nameTo, users, new String[] {});
      if (!rc)
      {
         return false;
      }
      
      // clone ACLs
      if (acls)
      {
         rc = cloneSubjectInACLs(nameFrom, nameTo);
         if (!rc)
         {
            return false;
         }
      }
      
      // recreate offline cache
      return currentCache();
   }
   
   /**
    * Changes the group by adding and/or removing users.
    * <p>
    * This API requires ADLU_GROUP_ACCESS permission.
    *
    * @param    name
    *           group name
    *
    * @param    additions
    *           array of user names to be added to the group; may be empty
    *
    * @param    deletions
    *           array of user names to be excluded from the group;
    *           may be empty
    *
    * @return   <code>true</code> if successfully changed the group
    */
   public boolean changeGroupUsers(String name, String[] additions, 
                                   String[] deletions)
   {
      // check the access rights
      if (!checkPermission("/accounts/users", AdminRights.ADLU_GROUP_ACCESS))
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // get and check the group account's ordinal number
      int go = sc.getOrdinalById(name);
      if (go == -1)
      {
         // no such account
         message("There is no account named '" + name + "'."); 
         return false;
      }
      
      Account acc = sc.getAccountByOrd(go);
      if (acc.getAccountType() != Account.ACC_GROUP)
      {
         // not a group account
         message("Account '" + name + "' is not a group."); 
         return false;
      }
      
      // trivial case of nothing to do
      if (additions.length == 0 && deletions.length == 0)
      {
         return true;
      }
      
      // do the essential part
      boolean rc = changeGroupUsersWorker(name, additions, deletions);
      
      // recreate offline cache
      if (rc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Returns an array of defined user account names and their owners.
    * <p>
    * This API requires ADLU_ENUMERATE_ACCESS permission.
    * 
    * @return   All defined user accounts or <code>null</code>
    */
   public TaggedName[] listUsers()
   {
      // check the access rights
      if (!checkPermission("/accounts/users", 
                           AdminRights.ADLU_ENUMERATE_ACCESS))
      {
         return null;
      }

      // check the cache
      if (!checkCache())
      {
         return null;
      }
         
      // get the list of user accounts
      String[] usernames = sc.getAllAccountIds(Account.ACC_USER);
      if (usernames == null)
      {
         return null;
      }

      // create tagged list
      TaggedName[] tu = new TaggedName[usernames.length];
      for (int i = 0; i < usernames.length; i++)
      {
         String username = usernames[i];
         UserAccount account = (UserAccount) sc.getAccountById(username);
         tu[i] = new TaggedName(username, 
                                account.getPerson() == null ? "" : account.getPerson(),
                                account.getOsUser() == null ? "" : account.getOsUser());
      }
      
      return tu;
   }
   
   /**
    * Returns the configuration information about the administration interface
    * itself. This information should be used to get access to the custom
    * admin extensions
    * 
    * @return   container with admin related configuration information
    */
   public AdminDef getAdminDef()
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }

      // check the cache
      if (!checkCache())
      {
         return null;
      }

      AdminDef def = new AdminDef();

      // set the custom client extension plugin
      def.customClientExt = sm.getCustomClientExt();

      // get the rights editor and the description for each resource
      def.customAclEditors    = new HashSet<>();
      def.resourceDescription = new HashMap<String, Description[]>();
      
      int nrRes = sc.getNumResources();
      
      // lookup the plugins description
      for (int i = 0; i < nrRes; i++)
      {
         AbstractResource plugin = sc.getRegistryByOrd(i).getPlugin();
         String type = plugin.getTypeName();
         def.customAclEditors.add(type);
         
         Description[] descr = plugin.describeRights();
         def.resourceDescription.put(type, descr);
      }      
      
      // create the admin profile for this session
      def.profile = new AdminProfile[prof.length];
      
      for (int i = 0; i < prof.length; i++)
      {
         def.profile[i] = new AdminProfile(prof[i],
               ar == null ? sr.checkAdminAccess() : ar.checkAnyAdminAccess(prof[i]));
      }
      
      return def;
   }
   
   /**
    * Returns the admin permissions profile for an arbitrary set of resource
    * instance names.
    * 
    * @param    name
    *           array of admin resource instance names to query
    * 
    * @return   permissions for the given names or <code>null</code>
    */
   public AdminProfile[] getProfile(String[] name)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      // check the parameter
      if (name == null)
      {
         message("Resource instance names can't be omitted");
         return null;
      }
      
      // create the admin profile for this set
      AdminProfile[] profile = new AdminProfile[name.length];
      
      for (int i = 0; i < name.length; i++)
      {
         profile[i] = new AdminProfile(name[i], 
                                           ar == null ? true :
                                           ar.checkAnyAdminAccess(name[i]));
      }
      
      return profile;
   }
   
   /**
    * Returns the admin permissions profile for an arbitrary set of
    * permissions for a resource instance name.
    * 
    * @param    name
    *           admin leaf resource instance name to query
    * 
    * @param    perm
    *           array of permissions to query
    * 
    * @return   permissions for the given name or <code>null</code>
    */
   public AdminProfile[] getProfile(String name, int[] perm)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      // check the parameter
      if (name == null)
      {
         message("Resource instance names can't be omitted");
         return null;
      }
      
      // create the admin profile for this set
      AdminProfile[] profile = new AdminProfile[perm.length];
      
      for (int i = 0; i < perm.length; i++)
      {
         profile[i] = new AdminProfile(name,
                          ar == null ? true :
                                       ar.checkAdminAccess(name, perm[i]) > 0);
      }
      
      return profile;
   }

   /**
    * Returns all OS user account definitions. This API requires one of the following permission 
    * ADLU_READ_ACCESS.
    *
    * @return   An array of all OS user account definitions.
    */
   public TaggedName[] listOsUsers()
   {
      // check the access rights
      if (!checkPermissions("/accounts/osusers", new int[]{ AdminRights.ADLU_READ_ACCESS }))
      {
         return null;
      }

      // check the cache
      if (!checkCache())
      {
         return null;
      }

      OsUserAccount[] osUserAccounts = sc.getAllOsUsers();
      
      TaggedName[] tn = new TaggedName[osUserAccounts.length];
      for (int i = 0; i < osUserAccounts.length; i++)
      {
         OsUserAccount osUserAccount = osUserAccounts[i];
         String desc = osUserAccount.getDescription();
         String hasPasswordSign = osUserAccount.getPassword() != null ? "✓" : "";
         tn[i] = new TaggedName(osUserAccount.getSubjectId(), desc == null ? "" : desc, hasPasswordSign);
      }

      return tn;
   }
   
   /**
    * Returns the OS user account definition for the specified account name. This API requires one of the 
    * following permission ADLU_READ_ACCESS.
    *
    * @param    name
    *           OS user account name
    *
    * @return   OS user account definition or <code>null</code> if no such OS user
    */
   public OsUserDef getOsUser(String name)
   {
      // check the access rights
      if (!checkPermissions("/accounts/osusers", new int[]{ AdminRights.ADLU_READ_ACCESS}))
      {
         return null;
      }

      // check the cache
      if (!checkCache())
      {
         return null;
      }

      // sanity check
      if (name == null)
      {
         message("OS user name can't be omitted.");
         return null;
      }

      // get the OS user account
      OsUserAccount ua = sc.getOsUserById(name);
      if (ua == null)
      {
         // no such account
         message("There is no OS user named '" + name + "'.");
         return null;
      }
      
      // create the user account definition
      OsUserDef ud = new OsUserDef();

      // fill it out
      ud.enabled = ua.isEnabled();
      ud.description = ua.getDescription();
      ud.protect = ua.isProtected();
      ud.subjectId = ua.getSubjectId();
      ud.password = ua.getPassword();

      Calendar caDate = Calendar.getInstance();
      if (ua.getPasswordDate() != null)
      {
         caDate.setTime(ua.getPasswordDate().getDate());
      }

      Calendar caTime = Calendar.getInstance();
      if (ua.getPasswordTime() != null)
      {
         caTime.setTime(ua.getPasswordTime().getDate());
      }

      caDate.set(Calendar.HOUR_OF_DAY, caTime.get(Calendar.HOUR_OF_DAY));
      caDate.set(Calendar.MINUTE, caTime.get(Calendar.MINUTE));
      caDate.set(Calendar.SECOND, caTime.get(Calendar.SECOND));
      caDate.set(Calendar.MILLISECOND, caTime.get(Calendar.MILLISECOND));

      ud.dateTime = caDate.getTime();
      return ud;
   }

   /**
    * Adds new OS user account definition.
    * <p>
    * The mandatory fields of the OS user definition container are validated.
    * Optional fields are added, depending on their presence in the container.
    * <p>
    * This API requires ADLU_CREATE_ACCESS permission.
    *
    * @param    user
    *           user account definition
    *
    * @return   <code>true</code> if successfully added the OS user
    */
   public boolean addOsUser(OsUserDef user)
   {
      String containerNode = "/accounts/osusers";
      // check the access rights
      if (!checkPermission(containerNode, AdminRights.ADLU_CREATE_ACCESS))
      {
         return false;
      }

      // check the cache
      if (!checkCache())
      {
         return false;
      }

      // get the existing account
      String name = user.subjectId;
      if (name == null)
      {
         message("Account name can't be omitted.");
         return false;
      }

      OsUserAccount acc = sc.getOsUserById(name);
      if (acc != null)
      {
         // name duplication
         message("OS user named '" + name + "' already exists.");
         return false;
      }

      String fullContainerNode = "/security/accounts/osusers";
      String fullContainerParentNode = "/security/accounts";
      if (ds.getNodeClass(fullContainerNode) == null)
      { 
         if (!ds.openBatch(fullContainerParentNode) ||
            !ds.addNode(fullContainerNode, "container", null))
         {
            message("Can't create container " + fullContainerNode + " in directory.");
            return false;
         }
         ds.closeBatch(true);
      }

      // open editing batch for the branch
      boolean rc = ds.openBatch(fullContainerNode);
      if (!rc)
      {
         message("Can't lock directory branch. Try later.");
         return false;
      };

      // build node ID
      String node = fullContainerNode + "/" + user.subjectId;
      
      if (user.password == null)
      {
         user.dateTime = null;
      }
      else
      {
         user.dateTime = new Date();
      }

      DateValue dateValue = user.dateTime == null ? null : new DateValue(user.dateTime);
      TimeValue timeValue = user.dateTime == null ? null : new TimeValue(user.dateTime);

      // create user node
      rc = createOsUserNode(node, user.description, user.enabled, user.password, dateValue, timeValue);

      // check directory operations
      if (!rc)
      {
         message("Directory modification failed for " + node);
      }

      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false);
      if (!rcc)
      {
         message("Error committing directory changes.");
      }

      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }

   /**
    * Changes the OS user account definition. Optional fields are synchronized with the directory. Password
    * aging information is set only if a password is set.
    * <p>
    * This API requires one of the following permissions:
    * <ul>
    *  <li>ADLU_WRITE_ACCESS 
    *  <li>ADLU_PASSWORD_ACCESS 
    * </ul>
    * The ADLU_PASSWORD_ACCESS permission allows changing the password and its
    * timestamp only. No validation of the rest of the user definition is done
    * in this case.
    *
    * @param    user
    *           user account definition
    *
    * @return   <code>true</code> if successfully changed the OS user
    */
   public boolean setOsUser(OsUserDef user)
   {
      // check the access rights
      String path = "/accounts/osusers";
      boolean permWr = queryPermission(path, AdminRights.ADLU_WRITE_ACCESS);
      boolean permPw = queryPermission(path, AdminRights.ADLU_PASSWORD_ACCESS);

      if (!permWr && !permPw)
      {
         message(String.format("insufficient access rights for %s (%d, %d)",
                               path,
                               AdminRights.ADLU_WRITE_ACCESS,
                               AdminRights.ADLU_PASSWORD_ACCESS));
         return false;
      }

      // check the cache
      if (!checkCache())
      {
         return false;
      }

      // get the existing account
      String name = user.subjectId;
      if (name == null)
      {
         message("OS user name can't be omitted.");
         return false;
      }

      OsUserAccount foundAcc = sc.getOsUserById(name);
      if (foundAcc == null)
      {
         // no such account
         message("There is no OS user named '" + name + "'.");
         return false;
      }

      // open editing batch for the branch
      String securityPath = "/security" + path;
      boolean rc = ds.openBatch(securityPath);
      if (!rc)
      {
         message("Can't lock directory branch. Try later.");
         return false;
      };

      // build node ID
      String node = securityPath  + "/" + user.subjectId;

      // sync the enabled attribute
      if (permWr && rc)
      {
         rc = syncBoolean(node, "enabled",  user.enabled);
      }

      // sync the description attribute
      if (rc)
      {
         rc = syncString(node, "description", user.description);
      }
      
      if ((user.password != null && !Arrays.equals(user.password, foundAcc.getPassword())) || 
         (user.password == null && !user.protect))
      {
         // delete password timestamp if no password given
         if (user.password == null)
         {
            user.dateTime = null;
         }
         else
         {
            user.dateTime = new Date();
         }
         
         // sync the password attribute
         if (rc)
         {
            rc = syncBytes(node, "password", user.password);
         }

         // sync the password set date attribute
         if (rc)
         {
            rc = syncDate(node, "pwsetdate", user.dateTime == null ? null : new DateValue(user.dateTime));
         }

         // sync the password set time attribute
         if (rc)
         {
            rc = syncTime(node, "pwsettime", user.dateTime == null ? null : new TimeValue(user.dateTime));
         }
      }

      // check directory operations
      if (!rc)
      {
         message("Directory modification failed for " + node);
      }

      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false);
      if (!rcc)
      {
         message("Error committing directory changes.");
      }

      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }

   /**
    * Deletes an OS user definition.
    * <p>
    * This API requires ADLU_DELETE_ACCESS permission.
    *
    * @param    name
    *           OS user name being deleted
    *
    * @return   <code>true</code> if successfully deleted the account
    */
   public boolean deleteOsUser(String name)
   {
      String path = "/accounts/osusers";
      // check the access rights
      if (!checkPermission(path, AdminRights.ADLU_DELETE_ACCESS))
      {
         return false;
      }

      // check the cache
      if (!checkCache())
      {
         return false;
      }

      // remove the user from ACLs
      if (!removeSubjectFromACLs(name))
      {
         // errors during ACLs modification
         return false;
      }

      String securityPath = "/security" + path;
      
      // open editing batch for the branch
      boolean rc = ds.openBatch(securityPath);
      if (!rc)
      {
         message("Can't lock directory branch. Try later.");
         return false;
      };

      // build the node name
      String node = securityPath + "/" + name;

      // remove the user node
      if (rc)
      {
         rc = ds.deleteNode(node);
      }

      // check directory operations
      if (!rc)
      {
         message("Directory modification failed for " + node);
      }

      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false);
      if (!rcc)
      {
         message("Error committing directory changes.");
      }

      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }

   /**
    * Returns an array of defined FWD user names, associated with the OS user.
    *
    * @param    osUser
    *           The OS user name
    *
    * @return   The associated FWD users or <code>null</code>
    */
   public TaggedName[] listAssociatedFwdUsers(String osUser)
   {
      // check the access rights
      if (!checkPermissions("/accounts/users", new int[]
         { AdminRights.ADLU_READ_ACCESS }))
      {
         return null;
      }

      // check the cache
      if (!checkCache())
      {
         return null;
      }

      // sanity check
      if (osUser == null)
      {
         message("OS user can't be omitted.");
         return null;
      }

      String[] allFwdUserIds = sc.getAllAccountIds(Account.ACC_USER);
      if (allFwdUserIds == null)
      {
         return null;
      }
      
      return Arrays.stream(allFwdUserIds)
                   .parallel()
                   .map(id -> (UserAccount) sc.getAccountById(id))
                   .filter(fwdUser -> osUser.equals(fwdUser.getOsUser()))
                   .map(fwdUser -> new TaggedName(fwdUser.getSubjectId()))
                   .toArray(TaggedName[]::new);
   }
   
   /**
    * Returns the user account definition for the specified account name.
    * <p>
    * This API requires one of the following permissions:
    * <ul>
    *  <li>ADLU_READ_ACCESS 
    *  <li>ADLU_WRITE_ACCESS 
    *  <li>ADLU_PASSWORD_ACCESS 
    * </ul>
    *
    * @param    name
    *           user account name
    *
    * @return   user account definition or <code>null</code> if no such user
    */
   @SuppressWarnings("null")
   public UserDef getUser(String name)
   {
      // check the access rights
      if (!checkPermissions("/accounts/users", new int[]
                            { AdminRights.ADLU_PASSWORD_ACCESS,
                              AdminRights.ADLU_READ_ACCESS,
                              AdminRights.ADLU_WRITE_ACCESS }))
      {
         return null;
      }
      
      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // sanity check
      if (name == null)
      {
         message("Account name can't be omitted."); 
         return null;
      }
      
      // get the user account
      Account acc = sc.getAccountById(name);
      if (acc == null)
      {
         // no such account
         message("There is no account named '" + name + "'."); 
         return null;
      }
      
      if (acc.getAccountType() != Account.ACC_USER)
      {
         // not a group account
         message("Account '" + name + "' is not a user."); 
         return null;
      }
      
      // create the user account definition
      UserDef ud = new UserDef();
      UserAccount ua = (UserAccount)acc;
      
      // fill it out
      ud.enabled = ua.isEnabled();
      ud.subjectId = ua.getSubjectId();
      ud.person = ua.getPerson();
      ud.alias = ua.getAlias();
      ud.protect = ua.isProtected();
      ud.password = ua.getPassword();
      ud.osUser = ua.getOsUser();
      
      Calendar caDate = Calendar.getInstance();
      if (ua.getPasswordDate() != null)
      {
         caDate.setTime(ua.getPasswordDate().getDate());
      }
      
      Calendar caTime = Calendar.getInstance();
      if (ua.getPasswordTime() != null)
      {
         caTime.setTime(ua.getPasswordTime().getDate());
      }
      
      caDate.set(Calendar.HOUR_OF_DAY, caTime.get(Calendar.HOUR_OF_DAY));
      caDate.set(Calendar.MINUTE, caTime.get(Calendar.MINUTE));
      caDate.set(Calendar.SECOND, caTime.get(Calendar.SECOND));
      caDate.set(Calendar.MILLISECOND, caTime.get(Calendar.MILLISECOND));
      
      ud.dateTime = caDate.getTime();

      if (ua.isModeInherited())
      {
         ud.mode = SecurityConstants.AUTH_MODE_NONE;
         ud.authPlugin = null;
      }
      else
      {
         ud.mode = ua.getAuthMode();
         ud.authPlugin = ua.getAuthPlugin();
      }
      
      // gather groups information
      int[] gids = ua.getGroups();
      int gs = gids == null ? 0 : gids.length;
      String[] gr = new String[gs];
      
      for (int i = 0; i < gs; i++)
      {
         gr[i] = sc.getAccountByOrd(gids[i]).getSubjectId();
      }
      
      ud.groups = gr;

      return ud;
   }

   /**
    * Changes the user account definition for the named account.
    * Optional fields are synchronized with the directory. Password aging
    * information is set only if a password is set.
    * If the account is password protected and the password is present, 
    * but both the date and time fields are NOT present, then they are 
    * automatically set to the current date and time.
    * <p>
    * This API requires one of the following permissions:
    * <ul>
    *  <li>ADLU_WRITE_ACCESS 
    *  <li>ADLU_PASSWORD_ACCESS 
    * </ul>
    * The ADLU_PASSWORD_ACCESS permission allows changing the password and its
    * timestamp only. No validation of the rest of the user definition is done
    * in this case.
    *
    * @param    user
    *           user account definition
    *
    * @param    ext
    *           optional extension container for custom account extensions
    *
    * @param    syncExt
    *           if <code>true</code>, the state of the account should sync'ed
    *           with the state of the extension container;
    *           otherwise the state of the account extensions does not change
    *
    * @return   <code>true</code> if successfully changed the account
    */
   public boolean setUser(UserDef user, Serializable ext, boolean syncExt)
   {
      // check the access rights
      boolean permWr = queryPermission("/accounts/users", 
                                       AdminRights.ADLU_WRITE_ACCESS);
      boolean permPw = queryPermission("/accounts/users", 
                                       AdminRights.ADLU_PASSWORD_ACCESS);

      if (!permWr && !permPw)
      {
         message(String.format("insufficient access rights for /accounts/users " 
                               + "(%d, %d)", AdminRights.ADLU_WRITE_ACCESS,
                               AdminRights.ADLU_PASSWORD_ACCESS));
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // get the existing account
      String name = user.subjectId;
      if (name == null)
      {
         message("Account name can't be omitted."); 
         return false;
      }
      
      Account acc = sc.getAccountById(name);
      if (acc == null)
      {
         // no such account
         message("There is no account named '" + name + "'."); 
         return false;
      }
      
      // ensure it's a user
      if (acc.getAccountType() != Account.ACC_USER)
      {
         // not a group account
         message("Account '" + name + "' is not a user."); 
         return false;
      }
      
      // validate auth mode
      if (permWr && 
         (user.mode < AUTH_MODE_LOWEST || user.mode > AUTH_MODE_HIGHEST)) 
      {
         // invalid data
         message("Authentication mode is invalid."); 
         return false;
      }
      
      // validate groups
      if (permWr && (user.groups != null))
      {
         for (int i = 0; i < user.groups.length; i++)
         {
            if (getGroup(user.groups[i]) == null)
            {
               message("User account references an invalid group '" +
                       user.groups[i] + "'."); 
               return false;
            }
         }
      }
      
      // assign password timestamp if omitted
      if (user.protect && user.password != null && user.dateTime == null)
      {
         user.dateTime = new Date();
      }
      
      // delete password timestamp if no password given
      if (user.password == null)
      {
         user.dateTime = null;
      }
      
      // open editing batch for the branch
      boolean rc = ds.openBatch("/security/accounts/users");
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build node ID
      String node = new String("/security/accounts/users/" + 
                               user.subjectId);
      
      // manage account extensions
      if (permWr && ae != null && syncExt)
      {
         try
         {
            rc = ae.setExtension(ds, node, ext);
         } 
         catch (AccountExtValidationException e)
         {
            // fast exit, validation failed
            ds.closeBatch(false, false);
            return false;
         }
         catch (Throwable t)
         {
            // unexpected failure
            message("can't complete operation. " + t);
            ds.closeBatch(false, false);
            return false;
         }
      }
      
      // sync the enabled attribute
      if (permWr && rc)
      {
         rc = syncBoolean(node, "enabled",  user.enabled);
      }
      
      // sync the person attribute
      if (permWr && rc)
      {
         rc = syncString(node, "person", user.person);
      }

      // change the osuser attribute
      if (permWr && rc)
      {
         rc = syncString(node, "osuser", user.osUser);
      }
      
      // sync the alias attribute
      if (permWr && rc)
      {
         rc = syncString(node, "alias", user.alias);
      }
      
      // change the groups attribute
      if (permWr && rc)
      {
         rc = syncStrings(node, "groups", user.groups);
      }

      // change auth mode settings
      if (permWr && rc)
      {
         rc = syncAuthSettings(node, user.mode, user.authPlugin);
      }

      // sync the password-protected attribute
      if (rc)
      {
         rc = syncBoolean(node, "protected", user.protect);
      }
      
      // sync the password attribute
      if (rc)
      {
         rc = syncBytes(node, "password", user.password);
      }
      
      // sync the password set date attribute
      // the null check is needed in case password has not been set for the account
      if (rc && user.dateTime != null)
      {
         rc = syncDate(node, "pwsetdate", new DateValue(user.dateTime));
      }
      
      // sync the password set time attribute
      // the null check is needed in case password has not been set for the account
      if (rc && user.dateTime != null)
      {
         rc = syncTime(node, "pwsettime", new TimeValue(user.dateTime));
      }

      // check directory operations
      if (!rc)
      {
         message("Directory modification failed for " + node);
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false);
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Adds new user account definition.
    * <p>
    * The mandatory fields of the user definition container are validated.
    * Optional fields are added, depending on their presence in the container.
    * One exception is the password timestamp: if the account is password
    * protected and the password is present, but both the date and time fields
    * are NOT present, then they are automatically set to the current date and
    * time.
    * <p>
    * This API requires ADLU_CREATE_ACCESS permission.
    *
    * @param    user
    *           user account definition
    *
    * @param    ext
    *           optional extension container for custom account extensions
    *
    * @return   <code>true</code> if successfully added the account
    */
   public boolean addUser(UserDef user, Serializable ext)
   {
      // check the access rights
      if (!checkPermission("/accounts/users", AdminRights.ADLU_CREATE_ACCESS))
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // get the existing account
      String name = user.subjectId;
      if (name == null)
      {
         message("Account name can't be omitted."); 
         return false;
      }
      
      Account acc = sc.getAccountById(name);
      if (acc != null)
      {
         // name duplication
         message("Account named '" + name + "' already exists."); 
         return false;
      }
      
      // validate auth mode
      if (user.mode < AUTH_MODE_LOWEST || user.mode > AUTH_MODE_HIGHEST) 
      {
         // invalid data
         message("Authentication mode is invalid."); 
         return false;
      }
      
      // validate groups
      if (user.groups != null)
      {
         for (int i = 0; i < user.groups.length; i++)
         {
            if (getGroup(user.groups[i]) == null)
            {
               message("User account references an invalid group '" +
                       user.groups[i] + "'."); 
               return false;
            }
         }
      }
      
      // assign password timestamp if omitted
      if (user.protect && user.password != null && user.dateTime == null)
      {
         user.dateTime = new Date();
      }
      
      // delete password timestamp if no password given
      if (user.password == null)
      {
         user.dateTime = null;
      }
      
      // open editing batch for the branch
      boolean rc = ds.openBatch("/security/accounts/users");
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build node ID
      String node = new String("/security/accounts/users/" + user.subjectId);

      DateValue dateValue = user.dateTime == null ? null : new DateValue(user.dateTime);
      TimeValue timeValue = user.dateTime == null ? null : new TimeValue(user.dateTime);

      // create user node
      rc = createUserNode(node, user.enabled, user.person, user.osUser, user.mode,
                          user.authPlugin, user.alias, user.groups, user.protect,
                          user.password, dateValue, timeValue);
      
      // manage account extensions
      if (rc && ae != null)
      {
         try
         {
            rc = ae.addExtension(ds, node, ext);
         }
         catch (AccountExtValidationException e)
         {
            // fast exit, validation failed
            ds.closeBatch(false, false);
            return false;
         }
         catch (Throwable t)
         {
            // unexpected failure
            message("can't complete operation. " + t);
            ds.closeBatch(false, false);
            return false;
         }
      }
      
      // check directory operations
      if (!rc)
      {
         message("Directory modification failed for " + node);
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Deletes a user account definition.
    * The account is deleted. All references to this account are deleted from
    * the ACLs, but the associated user certificate is left untouched.
    * <p>
    * The directory modification is done in multiple separate transactions:
    * <ul>
    *   <li>removing the user from the ACLs -
    *        <code>/security/acl</code> and <code>/security/acls</code>
    *   <li>removing the user account -
    *        <code>/security/accounts/users</code>
    * </ul>
    * This approach may leave the directory partially modified.
    * <p>
    * This API requires ADLU_DELETE_ACCESS permission.
    *
    * @param    name
    *           user account name being deleted
    *
    * @return   <code>true</code> if successfully deleted the account
    */
   public boolean deleteUser(String name)
   {
      // check the access rights
      if (!checkPermission("/accounts/users", AdminRights.ADLU_DELETE_ACCESS))
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // remove the user from ACLs
      if (!removeSubjectFromACLs(name))
      {
         // errors during ACLs modification
         return false;
      }
      
      // open editing batch for the branch
      boolean rc = ds.openBatch("/security/accounts/users");
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build the node name
      String node = new String("/security/accounts/users/" + name);
      
      // manage account extensions
      if (ae != null && ae.isExtended(ds, node))
      {
         try
         {
            rc = ae.deleteExtension(ds, node);
         } 
         catch (Throwable t)
         {
            // unexpected failure
            message("can't complete operation. " + t);
            ds.closeBatch(false, false);
            return false;
         }
      }
      
      // remove the user node
      if (rc)
      {
         rc = ds.deleteNode(node);
      }
      
      // check directory operations
      if (!rc)
      {
         message("Directory modification failed for " + node);
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false);
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
      
   /**
    * Clones an existing user account into a new one.
    * ACLs are modified to give the source user rights to the clone, if
    * this option is requested.
    * <p>
    * This API requires ADLU_CREATE_ACCESS permission.
    *
    * @param    nameFrom
    *           user account name for the source
    *
    * @param    nameTo
    *           user account name for the clone
    *
    * @param    acls
    *           <code>true</code> tells to clone ACLs as well
    *
    * @param    replacePassword
    *           <code>true</code> tells to assign the provided password
    *           instead of cloning it
    *
    * @param    newPassword
    *           a replacement password for the clone
    *
    * @return   <code>true</code> if successfully cloned the user account
    */
   public boolean cloneUser(String nameFrom, String nameTo, boolean acls,
                            boolean replacePassword, byte[] newPassword)
   {
      // check the access rights
      if (!checkPermission("/accounts/users", AdminRights.ADLU_CREATE_ACCESS))
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // get the source user
      UserDef su = getUser(nameFrom);
      
      if (su == null)
      {
         // no source
         return false;
      }
      
      if (replacePassword)
      {
         su.password = newPassword;
      }
      
      // open editing batch for the branch
      boolean rc = ds.openBatch("/security/accounts/users");
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build node ID
      String nodeFrom = new String("/security/accounts/users/" + nameFrom);
      String nodeTo = new String("/security/accounts/users/" + nameTo);
      
      // create the target user node
      rc = createUserNode(nodeTo, su.enabled, su.person, su.osUser, su.mode, su.authPlugin,
                          su.alias, su.groups, su.protect, su.password,
                          new DateValue(su.dateTime), new TimeValue(su.dateTime));
      
      // manage account extensions
      if (rc && ae != null && ae.isExtended(ds, nodeFrom))
      {
         try
         {
            rc = ae.cloneExtension(ds, nodeFrom, nodeTo);
         } 
         catch (AccountExtValidationException e)
         {
            // fast exit, validation failed
            ds.closeBatch(false, false);
            return false;
         }
         catch (Throwable t)
         {
            // unexpected failure
            message("can't complete operation. " + t);
            ds.closeBatch(false, false);
            return false;
         }
      }
      
      // check directory operations
      if (!rc)
      {
         message("Directory modification failed for " + nodeTo);
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
         return false;
      }
      
      if (!rc)
      {
         return false;
      }
         
      // clone ACLs
      if (acls)
      {
         rc = cloneSubjectInACLs(nameFrom, nameTo);
         if (!rc)
         {
            return false;
         }
      }
      
      // recreate offline cache
      rc = currentCache();
      return rc;
   }
   
   /**
    * Enumerates the groups the named user is part of.
    * <p>
    * This API requires one of the following permissions:
    * <ul>
    *  <li>ADLU_READ_ACCESS 
    *  <li>ADLU_WRITE_ACCESS 
    *  <li>ADLU_PASSWORD_ACCESS 
    * </ul>
    *
    * @param    name
    *           user account name
    *
    * @return   array of tagged group names, that may be empty, 
    *           or <code>null</code> if there is no such user etc
    */
   @SuppressWarnings("null")
   public TaggedName[] listUserGroups(String name)
   {
      // check the access rights
      if (!checkPermissions("/accounts/users", new int[]
                            { AdminRights.ADLU_PASSWORD_ACCESS,
                              AdminRights.ADLU_READ_ACCESS,
                              AdminRights.ADLU_WRITE_ACCESS }))
      {
         return null;
      }
      
      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // sanity check
      if (name == null)
      {
         message("Account name can't be omitted."); 
         return null;
      }
      
      // get the user account
      Account acc = sc.getAccountById(name);
      if (acc == null)
      {
         // no such account
         message("There is no account named '" + name + "'."); 
         return null;
      }
      
      if (acc.getAccountType() != Account.ACC_USER)
      {
         // not a group account
         message("Account '" + name + "' is not a user."); 
         return null;
      }
      
      // gather groups information
      UserAccount ua = (UserAccount)acc;
      int[] gids = ua.getGroups();
      int gs = gids == null ? 0 : gids.length;
      
      TaggedName[] gr = new TaggedName[gs];
      
      for (int i = 0; i < gs; i++)
      {
         gr[i] = new TaggedName(sc.getAccountByOrd(gids[i]).getSubjectId(),
                                sc.getAccountByOrd(gids[i]).getDescription());
      }
      
      return gr;
   }
   
   /**
    * Returns an array of resource plugin names.
    * 
    * @return   All defined resource plugin names 
    */
   public String[] listResourceNames()
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }

      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // get the size of the array first
      int nr = sc.getNumResources();
      if (nr == 0)
      {
         return new String[] {};
      }
      
      // get the plugin names
      String[] res = new String[nr];
      AbstractResource plugin = null;
      
      for (int i = 0; i < nr; i++)
      {
         plugin = sc.getRegistryByOrd(i).getPlugin();
         res[i] = new String(plugin.getClass().getName());
      }
      
      return res;
   }
   
   /**
    * Returns the registered resource type name for the given resource plugin.
    * 
    * @param    name
    *           plugin name
    * 
    * @return   registered resource type name or <code>null</code> if no match
    */
   public String getResourceType(String name)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }

      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // get the size of the array first
      int nr = sc.getNumResources();
      if (nr == 0)
      {
         return null;
      }
      
      // lookup the plugin name
      AbstractResource plugin = null;
      
      for (int i = 0; i < nr; i++)
      {
         plugin = sc.getRegistryByOrd(i).getPlugin();
         if (plugin.getClass().getName().equals(name))
         {
            return plugin.getTypeName();
         }
      }
      
      return null;
   }
   
   /**
    * Returns a tagged array of defined process names.
    * Tags are process descriptions.
    * 
    * @return   All defined group accounts or <code>null</code>
    */
   public TaggedName[] listProcesses()
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }

      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // get the list of accounts
      String[] pl = sc.getAllAccountIds(Account.ACC_PROCESS);
      if (pl == null)
      {
         return null;
      }

      // create tagged list
      TaggedName[] tp = new TaggedName[pl.length];
      for (int i = 0; i < pl.length; i++)
      {
         tp[i] = new TaggedName(new String(pl[i]), 
                                new String(sc.getAccountById(pl[i]).getDescription()));
      }
      
      return tp;
   }
   
   /**
    * Returns the process definition for the specified process name.
    * 
    * @param    name
    *           Process name.
    *           
    * @return   process definition or <code>null</code>
    */
   public ProcessDef getProcess(String name)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // sanity check
      if (name == null)
      {
         message("Account name can't be omitted."); 
         return null;
      }
      
      // get the process account
      Account acc = sc.getAccountById(name);
      if (acc == null)
      {
         // no such account
         message("There is no account named '" + name + "'."); 
         return null;
      }
      
      if (acc.getAccountType() != Account.ACC_PROCESS)
      {
         // not a group account
         message("Account '" + name + "' is not a process."); 
         return null;
      }
      
      // return the process definition
      ProcessAccount pa = (ProcessAccount)acc;
      ProcessDef pd = new ProcessDef();
      pd.enabled = pa.isEnabled();
      pd.subjectId = acc.getSubjectId();
      pd.description = acc.getDescription();
      pd.alias = acc.getAlias();
      pd.server = pa.isServer();
      pd.master = pa.isMaster();
      
      return pd;
   }
   
   /**
    * Changes the process definition for the named process.
    *
    * @param    process
    *           process definition
    *
    * @param    ext
    *           optional extension container for custom account extensions
    *
    * @param    syncExt
    *           if <code>true</code>, the state of the account should sync'ed
    *           with the state of the extension container;
    *          otherwise the state of the account extensions does not change
    *
    * @return   <code>true</code> if successfully changed the process
    */
   public boolean setProcess(ProcessDef process, Serializable ext, 
                             boolean syncExt)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // get and check the process account's ordinal number
      if (process.subjectId == null)
      {
         message("Account name can't be omitted."); 
         return false;
      }
      
      int po = sc.getOrdinalById(process.subjectId);
      if (po == -1)
      {
         // no such account
         message("There is no account named '" + process.subjectId + "'."); 
         return false;
      }
      
      Account acc = sc.getAccountByOrd(po);
      if (acc.getAccountType() != Account.ACC_PROCESS)
      {
         // not a process account
         message("Account '" + process.subjectId + "' is not a process."); 
         return false;
      }

      // open editing batch for the branch
      boolean rc = ds.openBatch("/security/accounts/processes");
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build node ID
      String node = new String("/security/accounts/processes/" + 
                               process.subjectId);
      
      // manage account extensions
      if (ae != null)
      {
         try
         {
            rc = ae.setExtension(ds, node, ext);
         } 
         catch (AccountExtValidationException e)
         {
            // fast exit, validation failed
            ds.closeBatch(false, false);
            return false;
         }
         catch (Throwable t)
         {
            // unexpected failure
            message("can't complete operation. " + t);
            ds.closeBatch(false, false);
            return false;
         }
      }
      
      // sync the enabled attribute
      if (rc)
      {
         rc = syncBoolean(node, "enabled", process.enabled);
      }
      
      // sync the description attribute
      if (rc)
      {
         rc = syncString(node, "description", process.description);
      }
      
      // sync the alias attribute
      if (rc)
      {
         rc = syncString(node, "alias", process.alias);
      }
      
      // sync the server attribute
      if (rc)
      {
         rc = syncBoolean(node, "server", process.server);
      }
      
      // sync the master attribute
      if (rc)
      {
         rc = syncBoolean(node, "master", process.server ?
                          Boolean.valueOf(process.master) : null);
      }
      
      // check directory operations
      if (!rc)
      {
         message("Directory modification failed for " + node);
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Adds new process account definition.
    *
    * @param    process
    *           process definition for a new account
    *
    * @param    ext
    *           optional extension container for custom account extensions
    *
    * @return   <code>true</code> if successfully added the process
    */
   public boolean addProcess(ProcessDef process, Serializable ext)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // get and check the existing accounts
      if (process.subjectId == null)
      {
         message("Account name can't be omitted."); 
         return false;
      }
      
      int ao = sc.getOrdinalById(process.subjectId);
      if (ao != -1)
      {
         // account exists
         message("Name '" + process.subjectId + 
                 "' duplicates existing account name.");
         return false;
      }
      
      // open editing batch for the branch
      boolean rc = ds.openBatch("/security/accounts/processes");
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build node ID
      String node = new String("/security/accounts/processes/" + 
                               process.subjectId);
      
      // create process node
      rc = createProcessNode(node, process.enabled, process.description,
                             process.alias, process.server, process.master);
      if (!rc)
      {
         message("Directory modification failed for " + node); 
      }
      
      // manage account extensions
      if (rc && ae != null)
      {
         try
         {
            rc = ae.addExtension(ds, node, ext);
         } 
         catch (AccountExtValidationException e)
         {
            // fast exit, validation failed
            ds.closeBatch(false, false);
            return false;
         }
         catch (Throwable t)
         {
            // unexpected failure
            message("can't complete operation. " + t);
            ds.closeBatch(false, false);
            return false;
         }
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false);
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Deletes a process account definition.
    * The account is deleted. All references to this process are deleted from
    * the ACLs, but the associated process certificate is left untouched.
    * <p>
    * The directory modification is done in multiple separate transactions:
    * <ul>
    *   <li>removing the process from the ACLs -
    *        <code>/security/acl</code> and <code>/security/acls</code>
    *   <li>removing the process account -
    *        <code>/security/accounts/processes</code>
    * </ul>
    * This approach may leave the directory partially modified.
    *
    * @param    name
    *           process account name being deleted
    *
    * @return   <code>true</code> if successfully deleted the process
    */
   public boolean deleteProcess(String name)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // remove the process from ACLs
      if (!removeSubjectFromACLs(name))
      {
         // errors during ACLs modification
         return false;
      }
      
      // open editing batch for the branch
      boolean rc = ds.openBatch("/security/accounts/processes");
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build the node name
      String node = new String("/security/accounts/processes/" + name);
      
      // manage account extensions
      if (ae != null && ae.isExtended(ds, node))
      {
         try
         {
            rc = ae.deleteExtension(ds, node);
         } 
         catch (Throwable t)
         {
            // unexpected failure
            message("can't complete operation. " + t);
            ds.closeBatch(false, false);
            return false;
         }
      }
      
      // remove the process node
      rc = ds.deleteNode(node);
      if (!rc)
      {
         message("Directory modification failed for " + node); 
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false);
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Clones an existing process account into a new one.
    * Master server flag is always false for the clone.
    * ACLs are modified to give the source process' rights to the clone, if
    * this option is requested.
    *
    * @param    nameFrom
    *           process name for the source
    *
    * @param    nameTo
    *           process name for the clone
    *
    * @param    acls
    *           <code>true</code> tells to clone ACLs as well
    *
    * @return   <code>true</code> if successfully cloned the process
    */
   public boolean cloneProcess(String nameFrom, String nameTo, boolean acls)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // get the source process
      ProcessDef sp = getProcess(nameFrom);
      
      if (sp == null)
      {
         // no source
         return false;
      }
      
      // open editing batch for the branch
      boolean rc = ds.openBatch("/security/accounts/processes");
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build node ID
      String nodeFrom = new String("/security/accounts/processes/" + nameFrom); 
      String nodeTo = new String("/security/accounts/processes/" + nameTo); 
      
      // create process node
      rc = createProcessNode(nodeTo, sp.enabled, sp.description, sp.alias, 
                             sp.server, sp.master);
      if (!rc)
      {
         message("Directory modification failed for " + nodeTo); 
      }
      
      // manage account extensions
      if (rc && ae != null && ae.isExtended(ds, nodeFrom))
      {
         try
         {
            rc = ae.cloneExtension(ds, nodeFrom, nodeTo);
         } 
         catch (AccountExtValidationException e)
         {
            // fast exit, validation failed
            ds.closeBatch(false, false);
            return false;
         }
         catch (Throwable t)
         {
            // unexpected failure
            message("can't complete operation. " + t);
            ds.closeBatch(false, false);
            return false;
         }
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false);
      if (!rcc)
      {
         message("Error committing directory changes.");
         return false;
      }
      
      if (!rc)
      {
         return false;
      }
      
      // clone ACLs
      if (acls)
      {
         rc = cloneSubjectInACLs(nameFrom, nameTo);
         if (!rc)
         {
            return false;
         }
      }
      
      // recreate offline cache
      rc = currentCache();
      return rc;
   }
   
   /**
    * Returns an array of defined CA or peer certificates with their
    * distinctive names.
    * 
    * @param    auth
    *           if <code>true</code>, list CA certificates. otherwise peer
    *           certificates
    * 
    * @param    unusedOnly
    *           if <code>true</code>, lists unused certificates only,
    *           otherwise all certificates
    * 
    * @return   All defined certificates of a given type or <code>null</code>
    */
   public TaggedName[] listCerts(boolean auth, boolean unusedOnly)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // quick return
      if (auth && unusedOnly)
      {
         return new TaggedName[]{};
      }
      
      // get the list of aliases of the type
      String als[] = auth ? sc.getCaAliases() : sc.getPeerAliases();
      
      // create the tagged list
      TaggedName[] ctn = new TaggedName[als.length];
      KeyStore ts = sc.getTrustStore();

      for (int i = 0; i < als.length; i++)
      {
         // access the certificate
         try
         {
            X509Certificate cert = (X509Certificate)ts.getCertificate(als[i]);
            
            // copy the distinctive name
            ctn[i] = new TaggedName(als[i], cert.getSubjectX500Principal().getName());
         }
         catch (KeyStoreException kse)
         {
            // never happens here
            message("Unexpected KeyStoreException.");
         }
         
      }
      
      if (auth || !unusedOnly)
      {
         return ctn;
      }
      
      // exclude assigned certificates from the collection
      List<TaggedName> lu = new LinkedList<TaggedName>();
      
      for (int i = 0; i < als.length; i ++)
      {
         TaggedName[] tn = listCertAccounts(als[i], Account.ACC_ALL);
         if (tn != null && tn.length > 0)
         {
            continue;
         }
         
         lu.add(ctn[i]);
      }
      
      return lu.toArray(new TaggedName[]{});
   }
   
   /**
    * Returns an array of user account names with their description for users
    * sharing this certificate.
    * 
    * @param    alias
    *           certificate alias name
    * 
    * @return   all user accounts referring to this certificate or
    *                     <code>null</code>
    */
   public TaggedName[] getCertUsers(String alias)
   {
      return listCertAccounts(alias, Account.ACC_USER);
   }
   
   /**
    * Returns an array of process account names with their description for
    * processes sharing this certificate.
    * 
    * @param    alias
    *           certificate alias name
    * 
    * @return   all process accounts referring to this certificate or
    *                     <code>null</code>
    */
   public TaggedName[] getCertProcesses(String alias)
   {
      return listCertAccounts(alias, Account.ACC_PROCESS);
   }
   
   /**
    * Returns an array of all account names with their description for
    * accounts sharing this certificate.
    * 
    * @param    alias
    *           certificate alias name
    * 
    * @return   all accounts referring to this certificate or <code>null</code>
    */
   public TaggedName[] getCertAccounts(String alias)
   {
      return listCertAccounts(alias, Account.ACC_ALL);
   }
   
   /**
    * Returns a DER-encoded certificate like those found in *.PEM files.
    * 
    * @param    alias
    *           the alias of the certificate to get
    * 
    * @return   the encoded certificate or <code>null</code>
    */
   public CertDef getCert(String alias)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // access the truststore
      KeyStore ts = sc.getTrustStore();
      X509Certificate cert = null;

      // get the certificate
      try
      {
         cert = (X509Certificate)ts.getCertificate(alias);
      }
      catch (KeyStoreException kse)
      {
         message("Unexpected KeyStoreException.");
         return null;
      }
      
      if (cert == null)
      {
         // no such certificate
         message("No certificate with alias '" + alias + "'.");
         return null;
      }
      
      // encode certificate and create definition
      CertDef cd = new CertDef();
      cd.alias = alias;
      cd.authority = false;
      
      // identify the type
      String als[] = sc.getCaAliases();
      
      for (int i = 0; i < als.length; i++)
      {
         if (als[i].equals(alias))
         {
            cd.authority = true;
            break;
         }
      }
      
      // get the encoded certificate
      String b64 = null;
      try
      {
         byte[] enc = cert.getEncoded();
         b64 = Base64.byteArrayToBase64(enc);
      }
      catch (CertificateEncodingException see)
      {
         message("Unexpected CertificateEncodingException.");
         return null;
      }
      
      // split the encoding into SPLIT_CHUNK sized chunks
      List<String> ls = new LinkedList<String>();
      ls.add(CERT_BEG);
      
      for (int i = 0; i < b64.length(); i += SPLIT_CHUNK)
      {
         int len = b64.length() - i >= SPLIT_CHUNK ? SPLIT_CHUNK : 
                                       b64.length() - i; 
         ls.add(b64.substring(i, i + len));
      }
                
      ls.add(CERT_END);
      cd.certificate = ls.toArray(new String[]{});
      
      return cd;
   }
   
   /**
    * Replaces an existing certificate with the given one.
    * 
    * @param    cert
    *           encoded certificate
    * 
    * @return   <code>true</code> if replaced, <code>false</code> if errors
    */
   public boolean setCert(CertDef cert)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // check and reassemble the certificate encoding
      if (cert.alias == null)
      {
         message("Certificate alias can't be omitted."); 
         return false;
      }
      
      byte[] enc = checkCertificate(cert);
      if (enc == null)
      {
         return false;
      }
      
      // open editing batch for the branch
      String branch = cert.authority ? "/security/certificates/cas" :
                                       "/security/certificates/peers";
      boolean rc = ds.openBatch(branch);
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build node ID
      String node = new String(branch + "/" + cert.alias); 
      
      // modify the directory
      rc = ds.setNodeByteArray(node, "value", enc);
      
      // check directory operations
      if (!rc)
      {
         message("Directory modification failed for " + node);
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Adds a new certificate according to its definition.
    * 
    * @param    cert
    *           encoded certificate
    * 
    * @return   <code>true</code> if added, <code>false</code> if errors
    */
   public boolean addCert(CertDef cert)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // check the alias for duplication
      if (cert.alias == null)
      {
         message("Certificate alias can't be omitted."); 
         return false;
      }
      
      if (!checkCAAlias(cert.alias))
      {
         message("Alias duplication in CA domain.");
         return false;
      }
      
      if (!checkPeerAlias(cert.alias))
      {
         message("Alias duplication in peer domain.");
         return false;
      }
      
      // check and reassemble the certificate encoding
      byte[] enc = checkCertificate(cert);
      if (enc == null)
      {
         // messages are generated in utility method
         return false;
      }
      
      // open editing batch for the branch
      String branch = cert.authority ? "/security/certificates/cas" :
                                       "/security/certificates/peers";
      boolean rc = ds.openBatch(branch);
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build node ID
      String node = new String(branch + "/" + cert.alias); 
      
      // modify the directory
      rc = createCertNode(node, enc);
      
      // check directory operations
      if (!rc)
      {
         message("Directory modification failed for " + node);
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Deletes an existing certificate conditionally or unconditionally.
    * Conditional deletion means the certificate belongs to the peer domain
    * and is not used by any account.
    * 
    * @param    alias
    *           the alias of the certificate being deleted
    * 
    * @param    force
    *           if <code>true</code>, the certificate is deleted even if in
    *           use
    * 
    * @return   <code>true</code> if deleted, <code>false</code> if errors
    * 
    */
   public boolean deleteCert(String alias, boolean force)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // find the certificate's domain
      boolean auth = false; 
      if (!checkCAAlias(alias))
      {
         // found in CA
         if (!force)
         {
            message("CA certificate with alias '" + alias +
                    "' can't be deleted.");
            return false;
         }
         
         auth = true;
      }
      
      if (checkPeerAlias(alias))
      {
         // not found in peers
         if (!auth)
         {
            message("No certificate with alias '" + alias + "'.");
            return false;
         }
      }
      else
      {
         // found in peers
         if (!force)
         {
            TaggedName[] tn = listCertAccounts(alias, Account.ACC_ALL);
            if (tn != null && tn.length > 0)
            {
               message("Peer certificate with alias '" + alias +
                       "' can't be deleted.");
               return false;
            }
         }
      }
      
      // open editing batch for the branch
      String branch = auth ? "/security/certificates/cas" :
                             "/security/certificates/peers";
      boolean rc = ds.openBatch(branch);
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build node ID
      String node = new String(branch + "/" + alias); 
      
      // modify the directory
      rc = ds.deleteNode(node);
      
      // check directory operations
      if (!rc)
      {
         message("Directory modification failed for " + node);
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Changes the certificate's alias.
    * 
    * @param    aliasFrom
    *           current certificate's alias
    * 
    * @param    aliasTo
    *           new certificate's alias
    * 
    * @return   <code>true</code> if changed, <code>false</code> if errors
    */
   public boolean moveCert(String aliasFrom, String aliasTo)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // both names are required
      if (aliasFrom  == null || aliasTo == null)
      {
         message("Aliases can't be omitted."); 
         return false;
      }
      
      // names must be different
      if (aliasFrom.equals(aliasTo))
      {
         message("Can't move alias to itself."); 
         return false;
      }
      
      // check the source alias for existence 
      if (checkCAAlias(aliasFrom) && checkPeerAlias(aliasFrom))
      {
         message("Source alias not found.");
         return false;
      }
      
      // check the target alias for duplication
      if (!checkCAAlias(aliasTo))
      {
         message("Alias duplication in CA domain.");
         return false;
      }
      
      if (!checkPeerAlias(aliasTo))
      {
         message("Alias duplication in peer domain.");
         return false;
      }
      
      // determine the alias' domain
      boolean peer = false;
      if (checkCAAlias(aliasFrom))
      {
         peer = true;
      }
      
      // open editing batch for the branch
      String branch = !peer ? "/security/certificates/cas" :
                              "/security/certificates/peers";
      boolean rc = ds.openBatch(branch);
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // build node ID
      String nodeFrom = new String(branch + "/" + aliasFrom); 
      
      // modify the directory
      rc = ds.moveNode(nodeFrom, branch + "/" + aliasTo);
      
      // check directory operations
      if (!rc)
      {
         message("Directory modification failed for " + nodeFrom);
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Searches the CA directory for an alias of the certificate by the
    * distinctive name of the subject.
    * 
    * @param    dn
    *           distinctive name
    * 
    * @return   alias of the certificate if exists, or <code>null</code>
    */
   public String searchCert(String dn)
   {
      // get the list of all certificates in CA domain
      String[] als = sc.getCaAliases();

      // get the truststore
      KeyStore ts = sc.getTrustStore();

      // do the direct search
      for (int i = 0; i < als.length; i++)
      {
         try
         {
            X509Certificate cert = (X509Certificate)ts.getCertificate(als[i]);
            
            String dna = cert.getSubjectX500Principal().getName();
            if (CertificateUtils.compareNames(dn, dna))
            {
               return als[i];
            }
         }
         catch (KeyStoreException kse)
         {
            return null;
         }
      }
     
      // no duplicate
      return null;
   }
   
   /**
    * Causes the live system to reload the P2J directory from the back
    * storage.
    * 
    * @return   <code>true</code> if successful
    */
   public boolean consReloadDirectory()
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }

      boolean rc = ds.atomicRefresh();
      
      // recreate offline cache
      if (rc)
      {
         rc = currentCache();
      }
      
      return rc;
   }
   
   /**
    * Creates a report of all current user and process sessions.
    * 
    * @return   Session descriptors suitable for reporting purposes or
    *           <code>null</code> if insufficient rights exist to obtain the
    *           list.
    */
   public SessionInfo[] getSessionList()
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      return sm.sessionSm.getSessionReport();
   }

   /**
    * Creates a report of a session matching the provided id.
    *
    * @param    id
    *           The context's session id of the {@link SecuritySession}.
    *           It can be retrieved through {@link SessionToken#getSessionID()}.
    *
    * @return   The session descriptor suitable for reporting purposes.
    *           <code>null</code> if insufficient rights exist to obtain it
    *           or the session was not found.
    */
   public SessionInfo getSessionById(int id)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }

      return sm.sessionSm.getSessionReportById(id);
   }
   
   /**                              
    * Forcibly terminates (closes) the socket in use for the given session. 
    *
    * @param    sid
    *           Session ID to terminate.
    */
   public void killSession(int sid)
   {
      // check the access rights
      if (!checkPermission())
      {
         throw new SecurityException("Insufficient rights to kill a session.");
      }

      try
      {
         sm.sessionSm.killSession(sid);
      }
      catch (RestrictedUseException e)
      {
         throw new SecurityException("Unable to kill a session due to restricted use.");
      }
   }
   
   /**
    * Locates all instances of ACLs in the directory and returns their names.
    * The shared instance has an empty name.
    * <p>
    * ACL instances are the directory branches that store ACLs. The shared
    * instance is the one found under <code>/security/acl</code> branch.
    * Private instances are found under
    * <code>/security/acls/<i>name</i></code> branches.
    * 
    * @return   array of ACL instance names; may be empty
    */
   public String[] listAclInstances()
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      return enumerateACLInstances();      
   }
   
   /**
    * Locates all instances of ACLs in the directory and returns their names
    * for those instances which have ACLs defined for the specified resource
    * type. The shared instance has an empty name.
    * <p>
    * ACL instances are the directory branches that store ACLs. The shared
    * instance is the one found under <code>/security/acl</code> branch.
    * Private instances are found under
    * <code>/security/acls/<i>name</i></code> branches.
    * 
    * @param    resourceType
    *           name of the resource of interest
    * 
    * @return   array of ACL instance names; may be empty
    */
   public String[] listAclInstances(String resourceType)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      // get the filtered list
      return enumerateACLInstances(resourceType);
   }
   
   
   /**
    * Lists resource names for which the specified ACL instance has ACLs
    * defined.
    * 
    * @param    instanceName
    *           name of the ACL instance; empty string for the shared one
    * 
    * @return   array of resource type names; may be empty
    */
   public String[] listInstanceAcls(String instanceName)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      // calculate the directory branch
      String node = instanceName.equals("") ? "/security/acl" :
                                              "/security/acls/" + instanceName;
                                          
      // enumerate resource types
      String[] rtypes = ds.enumerateNodes(node);
      if (rtypes == null)
      {
         return new String[0];
      }
      
      return rtypes;
   }
   
   /**
    * Gets the full ACL definition information from the directory for the
    * specified ACL instance and resource type.
    * 
    * @param    resourceType
    *           name of the resource of interest; can't be <code>null</code>
    * 
    * @param    instanceName
    *           name of the ACL instance; empty string for the shared one, or
    *           <code>null</code> for the effective list of this server
    * 
    * @return   ACL definition or <code>null</code>
    */
   public AclDef getAcl(String resourceType, String instanceName)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // check arguments
      if (resourceType == null)
      {
         message("Resource type can't me omitted");
         return null;
      }
      
      if (checkResourceType(resourceType) == -1)
      {
         return null;
      }
      
      // get the specified or shared instance first
      String path1 = instanceName == null || instanceName.length() == 0 ?
                     "/security/acl/"  + resourceType                   :
                     "/security/acls/" + instanceName + "/" + resourceType;
                     
      // conditionally get the additional path
      String path2 = instanceName == null ?
                     "/security/acls/" + sc.getServerAccount().getSubjectId() + 
                     "/" + resourceType   : null;
                     
      // get the ACL definition, possibly combined
      AclDef ad = getAclWorker(resourceType, instanceName, path1, path2);
      
      return ad;
   }
   
   /**
    * Gets the full ACL definition information from the directory for the
    * specified ACL instance or all of them.
    * 
    * @param    instanceName
    *           name of the ACL instance; empty string for the shared one; if
    *           <code>null</code>, all ACLs are returned and some may be
    *           combined
    * 
    * @return   an array of ACL definitions, one per resource type and instance,
    *           or <code>null</code>
    */
   public AclDef[] getAcls(String instanceName)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // create the scan list
      String[] inst = null;
      if (instanceName != null)
      {
         inst = new String[] {instanceName};
      }
      else
      {
         inst = enumerateACLInstances();
      }

      // process the list
      List<AclDef> ad = new LinkedList<AclDef>();
      String serverInstance = sc.getServerAccount().getSubjectId();
      
      for (int i = 0; i < inst.length; i++)
      {
         // if combining, do not return the private instance separately
         if (instanceName == null && inst[i].equals(serverInstance))
         {
            continue;
         }
         
         // base path
         String path = inst[i].length() == 0 ? "/security/acl"
                                             : "/security/acls/" + inst[i];
                                             
         // resource part
         String[] rtypes = listInstanceAcls(inst[i]);
         if (rtypes == null)
         {
            return null;
         }
         
         for (int j = 0; j < rtypes.length; j++)
         {
            // conditionally get the additional path
            String path2 = null;
            String instance = inst[i];
            
            if (instanceName == null && instance.equals(""))
            {
               // combining and this is the shared instance
               instance = null;
               path2 = "/security/acls/" + serverInstance + "/" + rtypes[j];
            }
                           
            AclDef acd = getAclWorker(rtypes[j], 
                                      instance, 
                                      path + "/" + rtypes[j], 
                                      path2);
            if (acd == null)
            {
               return null;
            }
            ad.add(acd);
         }
      }
      
      return ad.toArray(new AclDef[]{});
   }
   
   /**
    * Adds new set of ACLs based on the contents of the given ACL definition.
    * Both the target ACL instance and resource part will be created if they
    * don't exist.
    * 
    * @param    resourceType
    *           name of the resource to add the ACL for
    * 
    * @param    acl
    *           ACL definition, that should come fully defined, including the
    *           instance name.
    * 
    * @param    subjMap
    *           a map to translate numeric subject IDs back to names
    * 
    * @return   <code>true</code> if added, <code>false</code> if errors
    */
   @SuppressWarnings("null")
   public boolean addAcl(String resourceType, Acl acl, Map<Integer, String> subjMap)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // check arguments
      if (resourceType == null)
      {
         message("Resource type can't be omitted");
         return false;
      }
      
      // resource type should be known
      int ord = checkResourceType(resourceType);
      
      if (ord == -1)
      {
         return false;
      }
      
      // check the ACL in depth
      String[] subjects = null;
      if (acl != null && acl.subjects != null)
      {
         subjects = new String[acl.subjects.length];
      }
      
      if (!checkAcl(resourceType, acl, subjMap, subjects, 
                    true, true, true, true))
      {
         return false;
      }
         
      // calculate the directory branch to lock
      String branch =  acl.origin.equals("") ?
                       "/security/acl" : "/security/acls/" + acl.origin;
                       
      String save  = null;
      
      boolean createInstance = false;
      boolean createResource = false;
      
      if (ds.getNodeClass(branch) == null)
      {
         createInstance = true;
         createResource = true;
         
         // what to create
         save = branch;

         // what to lock
         branch = branch.substring(0, branch.lastIndexOf("/"));
      }
      
      // open editing batch for the branch
      boolean rc = ds.openBatch(branch);
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // create instance if it doesn't exist
      if (createInstance)
      {
         rc = ds.addNode(save, "container", null);
         if (!rc)
         {
            message(String.format("Can't create %s node", save)); 
         }
         
         branch = save;
      }
      
      // check if the resource exists
      branch = branch + "/" + resourceType;
      
      if (rc && !createResource)
      {
         if (ds.getNodeClass(branch) == null)
         {
            createResource = true;
         }
      }
      
      // create resource if it doesn't exist
      if (rc && createResource)
      {
         rc = ds.addNode(branch, "container", null);
         if (!rc)
         {
            message(String.format("Can't create %s node", branch)); 
         }
      }
      
      // build the ACL node ID and create the ACL container node
      String soid = null;
      String node = null;
      
      if (rc)
      {
         soid = String.format("%06d", acl.oid);
         node = String.format("%s/%s", branch, soid);
         
         if (ds.getNodeClass(node) != null)
         {
            message(String.format("Node %s already exists", node));
            rc = false;
         }
         else
         {
            rc = ds.addNode(node, "container", null);
            if (!rc)
            {
               message(String.format("Can't create %s node", node)); 
            }
         }
      }
      
      // create rights node
      if (rc)
      {
         rc = ds.addNodeAttributes(node + "/rights", acl.rights.getRightsName(),
                  acl.rights.getAttributes());
         if (!rc)
         {
            message(String.format("Can't create rights node for %s", node)); 
         }
      }
      
      // create resource instance node
      if (rc)
      {
         Attribute[] rdata = new Attribute[]
         {
            // resource instance name
            new Attribute(ds.getClassNodeAttribute("resource", 
                                                   "reference"),
                          new Object[] {acl.name}),
            // resource instance mode
            new Attribute(ds.getClassNodeAttribute("resource", 
                                                   "reftype"),
                          new Object[] {acl.exact}),
         };
         rc = ds.addNode(node + "/resource-instance", "resource", rdata);
         if (!rc)
         {
            message("can't add resource-instance to " + node);
         }
      }
               
      // create subjects node
      if (rc)
      {
         Attribute[] sdata = new Attribute[]
         {
            // subjects 
            new Attribute(ds.getClassNodeAttribute("strings", "values"),
                          subjects),
         };
         rc = ds.addNode(node + "/subjects", "strings", sdata);
         if (!rc)
         {
            message("can't add subjects to " + node);
         }
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // keep track of the added node; recreate offline cache
      if (rc && rcc)
      {
         mapTypes = mapInst.get(acl.origin);
         if (mapTypes == null)
         {
            mapTypes = new HashMap<String, Map<Integer, String>>();
            mapInst.put(acl.origin, mapTypes);
         }
         
         mapId = mapTypes.get(resourceType);
         if (mapId == null)
         {
            mapId = new HashMap<Integer, String>();
            mapTypes.put(resourceType, mapId);
         }
         
         mapId.put(acl.oid, soid);

         rc = currentCache();
         return rc;
      }

      return false;
   }

   /**
    * Get the list of available auth plugins. 
    *
    * @return  the list of available auth plugins encoded into tagged names or
    *          <code>null</code> if no plugins are available. 
    */
   public TaggedName[] listAuthPlugins()
   {
      // check the cache
      if (!checkCache())
      {
         return null;
      }

      AuthPlugin[] plugins = sc.getAuthPlugins();
      if (plugins == null || plugins.length == 0)
      {
         return null;
      }

      TaggedName[] taggedNames = new TaggedName[plugins.length];
      for (int i = 0; i < plugins.length; i++)
      {
         taggedNames[i] = plugins[i].toTaggedName();
      }

      return taggedNames;
   }

   /**
    * Preforms bulk update of authentication settings of the accounts of the
    * specified type.
    *
    * @param  targetAccounts
    *         Type of accounts to update. <code>Account.ACC_USER</code> and
    *         <code>Account.ACC_GROUP</code> are supported.
    * @param  excludedAccounts
    *         The list of IDs of the accounts that should NOT be updated. Can
    *         be <code>null</code>.
    * @param  authMode
    *         Authentication mode to set.
    * @param  authPlugin
    *         Authentication plugin to set.
    *
    * @return the number of updated accounts or <code>-1</code> if an error
    *         has occured.
    */
   public int bulkAuthModeUpdate(int targetAccounts,
                                 String[] excludedAccounts,
                                 int authMode,
                                 String authPlugin)
   {
      // validate auth mode
      if (authMode < AUTH_MODE_LOWEST || authMode > AUTH_MODE_HIGHEST)
      {
         message("Authentication mode is invalid.");
         return -1;
      }

      if (authMode != AUTH_MODE_CUSTOM)
         authPlugin = null;

      // check the cache
      if (!checkCache())
      {
         return -1;
      }

      int updatedNum = 0;

      Set<String> excludedSet = new HashSet<String>();
      if (excludedAccounts != null)
      {
         excludedSet.addAll(Arrays.asList(excludedAccounts));
      }

      if (targetAccounts == Account.ACC_USER)
      {
         String[] userList = sc.getAllAccountIds(Account.ACC_USER);
         if (userList == null || userList.length == 0)
            return 0;

         if (!queryPermission("/accounts/users",
                              AdminRights.ADLU_WRITE_ACCESS))
         {
            // insufficient access rights
            return -1;
         }

         // open editing batch for the branch
         boolean rc = ds.openBatch("/security/accounts/users");
         if (!rc)
         {
            message("Can't lock directory branch. Try later.");
            return -1;
         }

         for (String userId : userList)
         {
            if (excludedSet.contains(userId))
               continue;

            UserDef userDef = getUser(userId);
            if (userDef != null &&
                (userDef.mode != authMode ||
                 (userDef.authPlugin == null ? authPlugin != null :
                  !userDef.authPlugin.equals(authPlugin))))
            {
               // build node ID
               String node =
                     new StringBuilder("/security/accounts/users/").
                     append(userId).toString();

               // update auth settings
               rc = syncAuthSettings(node, authMode, authPlugin);

               if (rc)
                  updatedNum++;
            }
         }
      }
      else if (targetAccounts == Account.ACC_GROUP)
      {
         String[] groupList = sc.getAllAccountIds(Account.ACC_GROUP);
         if (groupList == null || groupList.length == 0)
            return 0;

         // check the access rights
         if (!checkPermission())
         {
            return -1;
         }

         // open editing batch for the branch
         boolean rc = ds.openBatch("/security/accounts/groups");
         if (!rc)
         {
            message("Can't lock directory branch. Try later.");
            return -1;
         }

         for (String groupId : groupList)
         {
            if (excludedSet.contains(groupId))
               continue;

            GroupDef groupDef = getGroup(groupId);
            if (groupDef != null &&
                (groupDef.mode != authMode ||
                 (groupDef.authPlugin == null ? authPlugin != null :
                  !groupDef.authPlugin.equals(authPlugin))))
            {
               // build node ID
               String node =
                     new StringBuilder("/security/accounts/groups/").
                     append(groupDef.subjectId).toString();

               // update auth settings
               rc = syncAuthSettings(node, authMode, authPlugin);

               if (rc)
                  updatedNum++;
            }
         }
      }
      else
      {
         return -1;
      }

      // close batch with commit or rollback and no refresh
      boolean rc = ds.closeBatch(updatedNum > 0, false);
      if (!rc)
      {
         message("Error committing directory changes.");
         return -1;
      }
      else
      {
         // recreate offline cache
         if (updatedNum > 0)
            currentCache();

         return updatedNum;
      }
   }

   /**
    * Send a refresh request to specified plugin.
    * 
    * @param resourceType
    */
   public void refreshPlugin(String resourceType)
   {
      int ord = checkResourceType(resourceType);
      
      AbstractResource plugin = sc.getRegistryByOrd(ord).getPlugin();
      
      if (plugin != null)
         plugin.refresh(this.ae);
   }
   
   /**
    * Conditionally edits the existing ACL based on the contents of the given
    * ACL and the selected conditions.
    * 
    * @param    resourceType
    *           name of the resource to set the ACL for
    * 
    * @param    acl
    *           ACL definition
    * 
    * @param    subjMap
    *           a map to translate numeric subject IDs back to names; needed
    *           only when setSubjects is <code>true</code> and can be
    *           <code>null</code> all other times
    * 
    * @param    setInstance
    *           <code>true</code> if setting the resource instance name
    * 
    * @param    setInstanceMode
    *           <code>true</code> if setting the resource instance name mode
    * 
    * @param    setSubjects
    *           <code>true</code> if setting the subjects IDs
    * 
    * @param    setRights
    *           <code>true</code> if setting the rights for this ACL
    * 
    * @return   <code>true</code> if added, <code>false</code> if errors
    */
   @SuppressWarnings("null")
   public boolean setAcl(String resourceType, Acl acl,
                         Map<Integer, String> subjMap,
                         boolean setInstance, boolean setInstanceMode,
                         boolean setSubjects, boolean setRights)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // check arguments
      if (resourceType == null)
      {
         message("Resource type can't be omitted");
         return false;
      }
      
      // resource type should be known
      int ord = checkResourceType(resourceType);
      
      if (ord == -1)
      {
         return false;
      }
      
      // check the ACL in depth
      String[] subjects = null;
      if (acl != null && acl.subjects != null)
      {
         subjects = new String[acl.subjects.length];
      }
      
      if (!checkAcl(resourceType, acl, subjMap, subjects, 
                    setInstance, setSubjects, setRights, false))
      {
         return false;
      }
      
      // find the target ACL container's parent
      String branch =  acl.origin.equals("") ?
                       "/security/acl" : "/security/acls/" + acl.origin;
      branch = branch + "/" + resourceType;
      
      // lookup the ACL container's oid (it has been checked for existence)
      mapTypes = mapInst.get(acl.origin);
      if (mapTypes == null)
      {
         mapTypes = new HashMap<String, Map<Integer, String>>();
         mapInst.put(acl.origin, mapTypes);
      }
      
      mapId = mapTypes.get(resourceType);
      if (mapId == null)
      {
         mapId = new HashMap<Integer, String>();
         mapTypes.put(resourceType, mapId);
      }
      
      String soid = mapId.get(acl.oid);
      String node = branch + "/" + soid;
      
      // open editing batch for the branch
      boolean rc = ds.openBatch(branch);
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // set the resource instance name
      if (setInstance)
      {
         String rnode = node + "/resource-instance";
         
         // make sure the node exists
         if (ds.getNodeClass(rnode) == null)
         {
            // create directory object
            Attribute[] data = new Attribute[]
            {
               // dummy reference
               new Attribute(ds.getClassNodeAttribute("resource", "reference"),
                             new Object[] {""}), 
               // reftype
               new Attribute(ds.getClassNodeAttribute("resource", "reftype"),
                             new Object[] {acl.exact}), 
            };
            rc = ds.addNode(rnode, "resource", data);
         }
         
         if (rc)
         {
            rc = syncString(rnode, "reference", acl.name);
         }
         
         if (!rc)
         {
            message("Can't set reference attribute of " + rnode); 
         }
      }
      
      // set the resource instance mode
      if (rc && setInstanceMode)
      {
         String rnode = node + "/resource-instance";
         
         // make sure the node exists
         if (ds.getNodeClass(rnode) == null)
         {
            // create directory object
            Attribute[] data = new Attribute[]
            {
               // reference
               new Attribute(ds.getClassNodeAttribute("resource", "reference"),
                             new Object[] {acl.name}), 
               // reftype
               new Attribute(ds.getClassNodeAttribute("resource", "reftype"),
                        new Object[] {acl.exact}), 
            };
            rc = ds.addNode(rnode, "resource", data);
         }
         
         if (rc)
         {
            rc = syncBoolean(rnode, "reftype", acl.exact);
         }
         
         if (!rc)
         {
            message("Can't set reftype attribute of " + rnode); 
         }
      }
      
      // set the subjects
      if (rc && setSubjects)
      {
         String snode = node + "/subjects";
         
         // make sure the node exists
         if (ds.getNodeClass(snode) == null)
         {
            // create directory object
            Attribute[] data = new Attribute[]
            {
            };
            rc = ds.addNode(snode, "strings", data);
         }
         
         if (rc)
         {
            rc = syncStrings(snode, "values", subjects);
         }
         
         if (!rc)
         {
            message("Can't set subjects as " + snode); 
         }
      }
      
      // set the rights
      if (rc && setRights)
      {
         String rnode = node + "/rights";
      
         // remove the old node first
         if (ds.getNodeClass(rnode) != null)
         {
            rc = Utils.removeDirectoryNode(ds, rnode);
            if (!rc)
            {
               message("Can't remove the existing node " + rnode); 
            }
         }
         
         if (rc)
         {
            rc = ds.addNodeAttributes(rnode, acl.rights.getRightsName(),
                     acl.rights.getAttributes());
            if (!rc)
            {
               message(String.format("Can't create rights node for %s", rnode)); 
            }
         }
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Moves the specified ACL to another position within the same ACL
    * instance.
    * 
    * @param    resourceType
    *           name of the resource of interest
    * 
    * @param    instanceName
    *           name of the source ACL instance; empty string for the shared
    *           one
    * 
    * @param    oid
    *           ACL object ID that selects the ACL to move
    * 
    * @param    newOid
    *           new ACL object ID
    * 
    * @return   <code>true</code> if moved, <code>false</code> if errors
    */
   public boolean moveAcl(String resourceType, String instanceName, int oid, 
                          int newOid)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // check arguments
      if (resourceType == null)
      {
         message("Resource type can't be omitted");
         return false;
      }
      
      // resource type should be known
      int ord = checkResourceType(resourceType);
      
      if (ord == -1)
      {
         return false;
      }

      // check the existence of the instance and the source ACL
      if (instanceName == null)
      {
         message("instance name can't be omitted");
         return false;
      }
      
      // check the existence of the instance, the source and target ACLs
      mapTypes = mapInst.get(instanceName);
      if (mapTypes == null)
      {
         message(String.format("Instance '%s' was not queried first", 
                               instanceName));
         return false;
      }
      
      mapId = mapTypes.get(resourceType);
      if (mapId == null)
      {
         message(String.format(
                 "Resource '%s' for instance '%s' was not queried first", 
                 resourceType, instanceName));
         return false;
      }
         
      String soid = mapId.get((oid));
      if (soid == null)
      {
         message("Source ACL does not exist or was not queried first");
         return false;
      }

      String toid = mapId.get((newOid));
      if (toid != null)
      {
         message(String.format(
                 "Target ACL should not exist, but '%s' was found", toid));
         return false;
      }
      
      // calculate directory path 
      String path = instanceName.length() == 0 ?
                    "/security/acl/"  + resourceType :
                    "/security/acls/" + instanceName + "/" + resourceType;
      
      // check the source node
      String snode = path + "/" + soid;
      String sclass = ds.getNodeClass(snode);
      
      if (sclass == null)
      {
         message(String.format("Source node '%s' was not found", snode)); 
         return false;
      };
      
      if (!sclass.equals("/meta/class/container"))
      {
         message(String.format(
                 "Found source node '%s' of unexpected class '%s'", 
                 snode, sclass)); 
         return false;
      };
      
      // check the target node
      toid = String.format("%06d", newOid);
      String tnode = path + "/" + toid;
      String tclass = ds.getNodeClass(tnode);
      
      if (tclass != null)
      {
         message(String.format("Target node '%s' already exists", tnode)); 
         return false;
      };
      
      // open editing batch for the branch
      boolean rc = ds.openBatch(path);
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };

      // move the ACL
      rc = ds.moveNode(snode, tnode);
      if (!rc)
      {
         message(String.format("Move '%s' to '%s' operation failed", snode, 
                               tnode)); 
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         // refresh the map: delete oid, add newOid
         mapId.remove(Integer.valueOf(oid));
         mapId.put(Integer.valueOf(newOid), toid);
         
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Moves the specified ACL to another ACL instance, private or shared. The
    * target instance will be created, if it doesn't exist.
    * 
    * @param    resourceType
    *           name of the resource of interest
    * 
    * @param    instanceName
    *           name of the source ACL instance; empty string for the shared
    *           one
    * 
    * @param    oid
    *           ACL object ID that selects the ACL to move
    * 
    * @param    newInstanceName
    *           name of the target ACL instance; empty string for the shared
    *           one
    * 
    * @return   <code>true</code> if moved, <code>false</code> if errors
    */
   public boolean moveAcl(String resourceType, String instanceName, int oid, 
                          String newInstanceName)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // check arguments
      if (resourceType == null)
      {
         message("Resource type can't be omitted");
         return false;
      }
      
      // resource type should be known
      int ord = checkResourceType(resourceType);
      
      if (ord == -1)
      {
         return false;
      }

      // check the existence of the instance and the source ACL
      if (instanceName == null || newInstanceName == null)
      {
         message("instance names can't be omitted");
         return false;
      }
      
      if (instanceName.equals(newInstanceName))
      {
         message("instance names should be different");
         return false;
      }
      
      mapTypes = mapInst.get(instanceName);
      if (mapTypes == null)
      {
         message(String.format("Instance '%s' was not queried first", 
                               instanceName));
         return false;
      }
      
      mapId = mapTypes.get(resourceType);
      if (mapId == null)
      {
         message(String.format(
                 "Resource '%s' for instance '%s' was not queried first", 
                 resourceType, instanceName));
         return false;
      }
         
      String soid = mapId.get((oid));
      if (soid == null)
      {
         message("Source ACL does not exist or was not queried first");
         return false;
      }

      // calculate directory paths 
      String sipath = instanceName.length() == 0 ?
                      "/security/acl" : "/security/acls/" + instanceName;
      String spath  = sipath + "/" + resourceType;
      
      String tipath = newInstanceName.length() == 0 ?
                      "/security/acl" : "/security/acls/" + newInstanceName;
      String tpath  = tipath + "/" + resourceType;
                     
      // check the source node
      String snode = spath + "/" + soid;
      String sclass = ds.getNodeClass(snode);
      
      if (sclass == null)
      {
         message(String.format("Source node '%s' was not found", snode)); 
         return false;
      };
      
      if (!sclass.equals("/meta/class/container"))
      {
         message(String.format(
                 "Found source node '%s' of unexpected class '%s'", 
                 snode, sclass)); 
         return false;
      };
      
      // check the target node
      String tnode = tpath + "/" + soid;
      String tclass = ds.getNodeClass(tnode);
      
      if (tclass != null)
      {
         message(String.format("Target node '%s' already exists", tnode)); 
         return false;
      };
      
      // calculate the directory branch to lock:
      // it is the common parent of the source resource node and
      // the parent of either the target instance or target resource node
      String branch = tpath;
                       
      boolean createInstance = false;
      boolean createResource = false;
      
      if (ds.getNodeClass(branch) == null)
      {
         createResource = true;
         branch = tipath;
      }
      
      if (ds.getNodeClass(branch) == null)
      {
         createInstance = true;
         branch = tipath.substring(0, tipath.lastIndexOf("/"));
      }
                       
      branch = commonParent(spath, branch);
      
      // open editing batch for the branch
      boolean rc = ds.openBatch(branch);
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };

      // create instance if it doesn't exist
      if (createInstance)
      {
         rc = ds.addNode(tipath, "container", null);
         if (!rc)
         {
            message(String.format("Can't create %s node", tipath)); 
         }
      }
      
      // create resource if it doesn't exist
      if (rc && createResource)
      {
         rc = ds.addNode(tpath, "container", null);
         if (!rc)
         {
            message(String.format("Can't create %s node", tpath)); 
         }
      }
      
      // move the ACL
      if (rc)
      {
         rc = ds.moveNode(snode, tnode);
         if (!rc)
         {
            message(String.format("Move '%s' to '%s' operation failed", snode, 
                                  tnode)); 
         }
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         // refresh the source map: delete oid
         mapId.remove(Integer.valueOf(oid));
         
         // refresh the target map" add newOid
         mapTypes = mapInst.get(newInstanceName);
         if (mapTypes == null)
         {
            mapTypes = new HashMap<String, Map<Integer, String>>();
            mapInst.put(newInstanceName, mapTypes);
         }
         
         mapId = mapTypes.get(resourceType);
         if (mapId == null)
         {
            mapId = new HashMap<Integer, String>();
            mapTypes.put(resourceType, mapId);
         }
         
         mapId.put(Integer.valueOf(oid), soid);
         
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Deletes the specified ACL from the specified ACL instance.
    * 
    * @param    resourceType
    *           name of the resource of interest
    * 
    * @param    instanceName
    *           name of the source ACL instance; empty string for the shared
    *           one
    * 
    * @param    oid
    *           ACL object ID that selects the ACL to delete
    * 
    * @return   <code>true</code> if deleted, <code>false</code> if errors
    */
   public boolean deleteAcl(String resourceType, String instanceName, int oid)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // check arguments
      if (resourceType == null)
      {
         message("Resource type can't be omitted");
         return false;
      }
      
      // resource type should be known
      int ord = checkResourceType(resourceType);
      
      if (ord == -1)
      {
         return false;
      }

      // check the existence of the instance
      if (instanceName == null)
      {
         message("instance name can't be omitted");
         return false;
      }
      
      mapTypes = mapInst.get(instanceName);
      if (mapTypes == null)
      {
         message(String.format("Instance '%s' was not queried first", 
                               instanceName));
         return false;
      }
      
      mapId = mapTypes.get(resourceType);
      if (mapId == null)
      {
         message(String.format(
                 "Resource '%s' for instance '%s' was not queried first", 
                 resourceType, instanceName));
         return false;
      }
         
      String soid = mapId.get((oid));
      if (soid == null)
      {
         message("Source ACL does not exist or was not queried first");
         return false;
      }

      // calculate directory path 
      String path = instanceName.length() == 0 ?
                    "/security/acl/"  + resourceType :
                    "/security/acls/" + instanceName + "/" + resourceType;
      
      // check the node
      String snode = path + "/" + soid;
      String sclass = ds.getNodeClass(snode);
      
      if (sclass == null)
      {
         message(String.format("Source node '%s' was not found", snode)); 
         return false;
      };
      
      if (!sclass.equals("/meta/class/container"))
      {
         message(String.format(
                 "Found source node '%s' of unexpected class '%s'", 
                 snode, sclass)); 
         return false;
      };
      
      // open editing batch for the branch
      boolean rc = ds.openBatch(path);
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };

      // delete the ACL
      rc = Utils.removeDirectoryNode(ds, snode);
      if (!rc)
      {
         message(String.format("Deleting '%s' failed", snode)); 
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         // refresh the map: delete oid
         mapId.remove(Integer.valueOf(oid));
         
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Clones the specified ACL to another position within the same ACL
    * instance.
    * 
    * @param    resourceType
    *           name of the resource of interest
    * 
    * @param    instanceName
    *           name of the source ACL instance; empty string for the shared
    *           one
    * 
    * @param    oid
    *           ACL object ID that selects the ACL to clone
    * 
    * @param    cloneOid
    *           ACL object ID for the clone
    * 
    * @return   <code>true</code> if cloned, <code>false</code> if errors
    */
   public boolean cloneAcl(String resourceType, String instanceName, int oid, 
                           int cloneOid)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // check arguments
      if (resourceType == null)
      {
         message("Resource type can't be omitted");
         return false;
      }
      
      // resource type should be known
      int ord = checkResourceType(resourceType);
      
      if (ord == -1)
      {
         return false;
      }

      // check the existence of the instance, the source and target ACLs
      // check the existence of the instance
      if (instanceName == null)
      {
         message("instance name can't be omitted");
         return false;
      }
      
      mapTypes = mapInst.get(instanceName);
      if (mapTypes == null)
      {
         message(String.format("Instance '%s' was not queried first", 
                               instanceName));
         return false;
      }
      
      mapId = mapTypes.get(resourceType);
      if (mapId == null)
      {
         message(String.format(
                 "Resource '%s' for instance '%s' was not queried first", 
                 resourceType, instanceName));
         return false;
      }
         
      String soid = mapId.get((oid));
      if (soid == null)
      {
         message("Source ACL does not exist or was not queried first");
         return false;
      }

      String toid = mapId.get((cloneOid));
      if (toid != null)
      {
         message(String.format(
                 "Target ACL should not exist, but '%s' was found", toid));
         return false;
      }
      
      // calculate directory path 
      String path = instanceName.length() == 0 ?
                    "/security/acl/"  + resourceType :
                    "/security/acls/" + instanceName + "/" + resourceType;
      
      // check the source node
      String snode = path + "/" + soid;
      String sclass = ds.getNodeClass(snode);
      
      if (sclass == null)
      {
         message(String.format("Source node '%s' was not found", snode)); 
         return false;
      };
      
      if (!sclass.equals("/meta/class/container"))
      {
         message(String.format(
                 "Found source node '%s' of unexpected class '%s'", 
                 snode, sclass)); 
         return false;
      };
      
      // check the target node
      toid = String.format("%06d", cloneOid);
      String tnode = path + "/" + toid;
      String tclass = ds.getNodeClass(tnode);
      
      if (tclass != null)
      {
         message(String.format("Target node '%s' already exists", tnode)); 
         return false;
      };
      
      // open editing batch for the branch
      boolean rc = ds.openBatch(path);
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };

      // copy the ACL
      rc = ds.copyNode(snode, tnode, true);
      if (!rc)
      {
         message(String.format("Copy '%s' to '%s' operation failed", snode, 
                               tnode)); 
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         // refresh the map: add cloneOid
         mapId.put(Integer.valueOf(cloneOid), toid);
         
         rc = currentCache();
         return rc;
      }

      return false;
   }
   
   /**
    * Assigns new object IDs sequentially for all ACLs in the given ACL
    * instance and resource type.
    * 
    * @param    resourceType
    *           name of the resource of interest
    * 
    * @param    instanceName
    *           name of the source ACL instance; empty string for the shared
    *           one
    * 
    * @param    start
    *           the starting number
    * 
    * @param    step
    *           the increment
    * 
    * @return   <code>true</code> if success, <code>false</code> if errors
    */
   @SuppressWarnings("null")
   public boolean resequenceAcls(String resourceType, String instanceName,  
                                 int start, int step)
   {
      // check the access rights
      if (!checkPermission())
      {
         return false;
      }
      
      // check the cache
      if (!checkCache())
      {
         return false;
      }
      
      // check arguments
      if (resourceType == null)
      {
         message("Resource type can't be omitted");
         return false;
      }
      
      // resource type should be known
      int ord = checkResourceType(resourceType);
      
      if (ord == -1)
      {
         return false;
      }

      // check the existence of the instance, the source and target ACLs
      // check the existence of the instance
      if (instanceName == null)
      {
         message("instance name can't be omitted");
         return false;
      }
      
      // calculate directory path 
      String path = instanceName.length() == 0 ?
                    "/security/acl/"  + resourceType :
                    "/security/acls/" + instanceName + "/" + resourceType;
      
      // open editing batch for the branch
      boolean rc = ds.openBatch(path);
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };

      // enumerate and sort existing ACLs
      // create a combination map: Integer oid to ACL path
      SortedMap<Integer, String> amap = new TreeMap<Integer, String>();
      
      // enumerate ACLs under the mandatory path
      int badIds = enumerateACLs(path, amap, 0);
      String[] acls = null;
      
      if (badIds > 0)
      {
         message(String.format("fix %d bad ACL IDs before resequencing", 
                               badIds)); 
         rc = false;
      }
      else
      {
         // create sorted array of ACL names
         acls = new String[amap.size()];
         Set<Integer> set = amap.keySet();
         Iterator<Integer> iter = set.iterator();
         
         int i = 0;
         while (iter.hasNext())
         {
            Integer k = iter.next();
            String base = amap.get(k);
            String oid = base.substring(base.lastIndexOf("/") + 1);
            acls[i++] = oid;
         }
      }
      
      // rename existing nodes to temporary names
      if (rc)
      {
         for (int i = 0; i < acls.length; i++)
         {
            rc = ds.moveNode(path + "/" + acls[i], path + "/z" + acls[i]);
            if (!rc)
            {
               message("temporary ACL rename failed for " + 
                       path + "/" + acls[i]); 
               break;
            }
         }
      }
      
      // resequence existing nodes permanently
      Map<Integer, String> tempMap = new HashMap<Integer, String>();
      
      if (rc)
      {
         int nseq = start;
         
         for (int i = 0; i < acls.length; i++)
         {
            String seq = String.format("%06d", nseq);
            tempMap.put(Integer.valueOf(nseq), seq);
                                       
            rc = ds.moveNode(path + "/z" + acls[i], path + "/" + seq);
            if (!rc)
            {
               message("ACL rename failed."); 
               break;
            }
            
            nseq += step;
         }
      }
      
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(rc, false); 
      if (!rcc)
      {
         message("Error committing directory changes.");
      }
      
      // recreate offline cache
      if (rc && rcc)
      {
         // refresh the map
         mapTypes = mapInst.get(instanceName);
         if (mapTypes == null)
         {
            mapTypes = new HashMap<String, Map<Integer, String>>();
            mapInst.put(instanceName, mapTypes);
         }
         
         mapId = mapTypes.get(resourceType);
         if (mapId == null)
         {
            mapId = new HashMap<Integer, String>();
            mapTypes.put(resourceType, mapId);
         }
         
         mapId.clear();
         mapId.putAll(tempMap);
         
         rc = currentCache();
         return rc;
      }

      return false;
   }

   /**
    * Account reports extension point.
    *
    * @return   An instance of {@link AccountReportsExtension} or <code>null</code> if no
    *           extension is present.
    */
   public AccountReportsExtension getAccountReportsExtension()
   {
      return ae != null ? ae.getAccountReportsExtension() : null;
   }

   /**
    * Checks an ACL for validity.
    * 
    * @param    resourceType
    *           name of the resource to add the ACL for
    * 
    * @param    acl
    *           ACL definition
    * 
    * @param    subjMap
    *           a map to translate numeric subject IDs back to names
    * 
    * @param    subjects
    *           array to store the subject names
    * 
    * @param    setInstance
    *           <code>true</code> if setting the resource instance name
    * 
    * @param    setSubjects
    *           <code>true</code> if setting the subjects IDs
    * 
    * @param    setRights
    *           <code>true</code> if setting the rights for this ACL
    * 
    * @param    addOrSet
    *           <code>true</code> if adding new ACL, else setting existing ACL
    * 
    * @return   <code>true</code> if valid
    */
   private boolean checkAcl(String resourceType, Acl acl, 
                            Map<Integer, String> subjMap, String[] subjects,
                            boolean setInstance, boolean setSubjects, 
                            boolean setRights,   boolean addOrSet)
   {
      // ACL should be present
      if (acl == null)
      {
         message("ACL can't me omitted");
         return false;
      }
      
      // subject map should be present
      if (setSubjects && subjMap == null)
      {
         message("Subject map can't be omitted");
         return false;
      }

      // subject map should be present
      if (setSubjects)
      {
         if (subjMap == null)
         {
            message("Subject map can't be omitted");
            return false;
         }

         if (subjMap.isEmpty())
         {
            message("Subject map must not be empty");
            return false;
         }
      }

      // check ACL details
      
      // origin should be present
      if (acl.origin == null)
      {
         message("Target ACL instance can't be omitted");
         return false;
      }
      
      // resource name should be present
      if (setInstance && acl.name == null)
      {
         message("Target ACL resource name can't be omitted");
         return false;
      }
      
      // subjects should be present
      if (setSubjects && acl.subjects == null)
      {
         message("Target ACL subjects can't be omitted");
         return false;
      }
      
      // rights should be present
      if (setRights && acl.rights == null)
      {
         message("Target ACL rights can't be omitted");
         return false;
      }
      
      // check the oid - should be valid and new (add) or valid and existing
      if (acl.oid < 0)
      {
         message("Target ACL ID can't be negative");
         return false;
      }
      
      boolean exists = false;
   
      mapTypes = mapInst.get(acl.origin);
      if (mapTypes == null)
      {
         mapTypes = new HashMap<String, Map<Integer, String>>();
         mapInst.put(acl.origin, mapTypes);
      }
      
      mapId = mapTypes.get(resourceType);
      if (mapId == null)
      {
         mapId = new HashMap<Integer, String>();
         mapTypes.put(resourceType, mapId);
      }
         
      if (mapId.containsKey(Integer.valueOf(acl.oid)))
      {
         exists = true;
      }
      
      if (addOrSet && exists)
      {
         message("Target ACL ID should not exist");
         return false;
      }
      else if (!addOrSet && !exists)
      {
         message("Target ACL ID should have been queried first");
         return false;
      }
      
      // check the resource name with the resource plugin, if exact
      if (setInstance && acl.exact)
      {
         // locate the plugin responsible for this resource type
         int ord = checkResourceType(resourceType);
         AbstractResource plugin = sc.getRegistryByOrd(ord).getPlugin();
         
         if (!plugin.isInstanceNameValid(acl.name))
         {
            message("Target ACL resource name is not acceptable");
            return false;
         }
      }
      
      // check subject names for validity and duplication
      if (setSubjects)
      {
         for (int i = 0; i < acl.subjects.length; i++)
         {
            // special case of ALL_OTHERS
            if (acl.subjects[i] == AclDef.ALL_OTHERS)
            {
               if (acl.subjects.length != 1)
               {
                  message("ALL_OTHERS must be the only subject");
                  return false;
               }
               
               subjects[i] = "all_others";
               continue;
            }
            
            // map numeric ID to name
            String subj = subjMap.get(Integer.valueOf(acl.subjects[i]));
            if (subj == null)
            {
               message(String.format(
                       "Target ACL subject %d is either unmapped or duplicate",
                       acl.subjects[i]));
               return false;
            }
            
            // check the name being a valid account
            Account acc = sc.getAccountById(subj);
            if (acc == null)
            {
               message(String.format("Target ACL subject %s does not exist", 
                                     subj));
               return false;
            }
            
            // add name to the array
            subjects[i] = subj;
         }
      }
      
      // everything is perfect
      return true;
   }
   
   /**
    * Checks the resource type name for validity.
    * 
    * @param    resourceType
    *           resource type name
    * 
    * @return   registered resource type ordinal or <code>-1</code> if no match
    */
   private int checkResourceType(String resourceType)
   {
      // get the size of the array first
      int nr = sc.getNumResources();
      if (nr == 0)
      {
         message("No resource types defined at all");  
         return -1;
      }
      
      // lookup the resource plugin table 
      AbstractResource plugin = null;
      
      for (int i = 0; i < nr; i++)
      {
         plugin = sc.getRegistryByOrd(i).getPlugin();
         if (plugin.getTypeName().equals(resourceType))
         {
            return i;
         }
      }
      
      message("Unknown resource type '" + resourceType + "'");
      return -1;
   }
   
   /**
    * Checks certificate alias for duplication in CA domain.
    * 
    * @param    alias
    *           certificate alias
    * 
    * @return   <code>true</code> if there is no duplicate
    */
   private boolean checkCAAlias(String alias)
   {
      // get the list in CA domain
      String[] als = sc.getCaAliases();
      
      // check for duplication
      for (int i = 0; i < als.length; i++)
      {
         if (als[i].equals(alias))
         {
            // found duplicate
            return false;
         }
      }
     
      // no duplicate
      return true;
   }
   
   /**
    * Checks certificate alias for duplication in peer domain.
    * 
    * @param    alias
    *           certificate alias
    * 
    * @return   <code>true</code> if there is no duplicate
    */
   private boolean checkPeerAlias(String alias)
   {
      // get the list in peer domain
      String[] als = sc.getPeerAliases();
      
      // check for duplication
      for (int i = 0; i < als.length; i++)
      {
         if (als[i].equals(alias))
         {
            // found duplicate
            return false;
         }
      }
     
      // no duplicate
      return true;
   }
   
   /**
    * Reassembles certificate encoding from its definition. Verifies the
    * certificate's domain and its validity.
    * <p>
    * The domain verification is based on the presence and contents of the
    * BasicConstraints X.509 extension, object ID 2.5.29.19. To avoid using a
    * DER decoder, this particular case takes a shortcut, based on the
    * knowledge of the expected result.
    * <p>
    * First, the result of the getExtensionValue() is always an OCTET STRING.
    * Second, the BasicConstraints value is
    * 
    * <pre>
    *    BasicConstraints ::= SEQUENCE {
    *         cA                      BOOLEAN DEFAULT FALSE,
    *         pathLenConstraint       INTEGER (0..MAX) OPTIONAL }
    * </pre>
    * 
    * Thus, the DER encoding should look like this:
    * <ul>
    * <li>- 4 or more bytes
    * <li>- byte 0 is 0x04 typically for OCTET STRING
    * <li>- byte 1 encodes the length of the rest
    * <li>- byte 2, if present, is 0x30 for SEQUENCE
    * <li>- byte 3, if present, is a BOOLEAN cA
    * </ul>
    * 
    * @param    cert
    *           certificate definition
    * 
    * @return   array of bytes of the encoded certificate or <code>null</code>
    */
   private byte[] checkCertificate(CertDef cert)
   {
      // check and reassemble the certificate encoding
      int len = cert.certificate.length - 2;
      if (len < 1                               ||
          !cert.certificate[0].equals(CERT_BEG) ||
          !cert.certificate[len + 1].equals(CERT_END))
      {
         message("Certificate definition format is bad."); 
         return null;
      }
      
      StringBuilder sb = new StringBuilder();
      for (int i = 1; i < len + 1; i++)
      {
         if ((i != len && cert.certificate[i].length() != SPLIT_CHUNK) ||
             (i == len && cert.certificate[i].length() > SPLIT_CHUNK))
         {
            message("Certificate definition format is bad."); 
            return null;
         }
         
         sb.append(cert.certificate[i]);
      }
      
      // get the binary encoding
      byte[] bin = Base64.base64ToByteArray(sb.toString());
      ByteArrayInputStream bis = new ByteArrayInputStream(bin);
      
      // decode the certificate
      X509Certificate xcert = null;
      try
      {
         CertificateFactory cf = CertificateFactory.getInstance("X.509");
         xcert = (X509Certificate)cf.generateCertificate(bis);
         bis.close();
      }
      catch (CertificateException ce)
      {
         return null;
      }
      catch (IOException ioe)
      {
      }
      finally
      {
         try
         {
            bis.close();
         }
         catch (IOException ioe)
         {
         }
      }
      
      // verify certificate's domain inspecting the BasicConstraints extension
      byte[] bc = xcert.getExtensionValue("2.5.29.19");
      boolean cA = false;
      
      // DER-decoding shortcut:
      if (bc.length >= 4 && bc[3] != 0)
      {
         cA = true;
      }
      
      if (cert.authority)
      {
         // for CA certificate, the BasicConstraints extension is required
         // and should indicate CA:TRUE

         if (!cA)
         {
            message("Certificate is not of a Certificate Authority"); 
            return null;
         }
      }
      else
      {
         // for peer certificate, the BasicConstraints extension is not
         // required, but if present, should indicate CA:FALSE

         if (cA)
         {
            message("Certificate is of a Certificate Authority"); 
            return null;
         }
      }
      
      // certificate is acceptable
      return bin;
   }
   
   /**
    * Returns an array of account names with their description for accounts
    * sharing this certificate.
    * 
    * @param    alias
    *           certificate alias name
    * 
    * @param    type
    *           account type
    * 
    * @return   all accounts referring to this certificate or <code>null</code>
    */
   private TaggedName[] listCertAccounts(String alias, int type)
   {
      // check the access rights
      if (!checkPermission())
      {
         return null;
      }
      
      // check the cache
      if (!checkCache())
      {
         return null;
      }
      
      // list all accounts of the type
      String[] al = sc.getAllAccountIds(type);
      if (al == null)
      {
         return null;
      }
      
      // select by alias
      List<TaggedName> ltn = new LinkedList<TaggedName>();
      
      for (int i = 0; i < al.length; i++)
      {
         Account acc = sc.getAccountById(al[i]);
         String aalias = acc.getAlias();
         if (aalias != null && aalias.equals(alias))
         {
            TaggedName tn = new TaggedName(al[i], acc.getDescription());
            ltn.add(tn);
         }
      }
      
      return ltn.toArray(new TaggedName[]{});
   }
   
   /**
    * Creates new security cache generation offline.
    * 
    * @return   <code>true</code> if cache is OK
    */
   private boolean currentCache()
   {
      try
      {
         sc = new SecurityCache(sm.getConfig(), -1, SecurityCache.LM_ADMIN, null);
      }
      catch (ConfigurationException cfgx)
      {
         sc = null;
         message(new String[] {cfgx.getMessage()});
         return false;
      }
      catch (NoSuchMethodException nsmx)
      {
         sc = null;
         message(new String[] {nsmx.getMessage()});
         return false;
      }
      
      sc.refreshAll(this.ae);
      
      return true;
   }
   
   /**
    * Checks to see if the cache is available and stores a message if not.
    * 
    * @return   <code>true</code> if cache is OK
    */
   private boolean checkCache()
   {
      if (sc == null)
      {
         message("Last security cache reconstruction attempt has failed.");
         return false;
      }
   
      return true;
   }
   
   /**
    * Changes the group by adding and/or removing users.
    *
    * @param    name
    *           group name
    *
    * @param    additions
    *           array of user names to be added to the group; may be empty
    *
    * @param    deletions
    *           array of user names to be excluded from the group;
    *           may be empty
    *
    * @return   <code>true</code> if successfully changed the group
    */
   private boolean changeGroupUsersWorker(String name, String[] additions, 
                                          String[] deletions)
   {
      // get the existing users of the group
      TaggedName[] tusers = listGroupUsers(name);
      if (tusers == null)
      {
         // inconsistency found
         message("Directory inconsistency found when listing users of group '"
                 + name + "'.");
         return false;
      }
      
      // open editing batch for the branch
      boolean rc = ds.openBatch("/security/accounts/users");
      if (!rc)
      {
         message("Can't lock directory branch. Try later."); 
         return false;
      };
      
      // process additions
      for (int i = 0; i < additions.length; i++)
      {
         String newUser = additions[i];
         
         // check for valid user name
         Account ua = sc.getAccountById(newUser);
         if (ua == null || ua.getAccountType() != Account.ACC_USER)
         {
            message("There is no user account named '" + newUser + "'."); 
            ds.closeBatch(false, false);
            return false;
         }
         
         // check for duplication
         for (int n = 0; n < tusers.length; n++)
         {
            if (tusers[n].getName().compareToIgnoreCase(newUser) == 0)
            {
               message("User '" + newUser + "' is in the group yet.");
               ds.closeBatch(false, false);
               return false;
            }
         }
         
         // OK to add this user to the group
         if (!ds.addNodeString("/security/accounts/users/" + newUser, 
                               "groups", name))
         {
            message("Directory modification failed for " + 
                    "/security/accounts/users/" + newUser);
            ds.closeBatch(false, false);
            return false;
         }
      }
      
      // process deletions
      for (int i = 0; i < deletions.length; i++)
      {
         String delUser = deletions[i];
         
         // check for valid user name
         Account ua = sc.getAccountById(delUser);
         if (ua == null || ua.getAccountType() != Account.ACC_USER)
         {
            message("There is no user account named '" + delUser + "'."); 
            ds.closeBatch(false, false);
            return false;
         }
         
         // check for presence in the group
         int n = 0;
         for (n = 0; n < tusers.length; n++)
         {
            if (tusers[n].getName().compareToIgnoreCase(delUser) == 0)
            {
               break;
            }
         }
         
         if (n == tusers.length)
         {
            // not in the group; ignore it
            message("User '" + tusers[n].getName() + "' not in the group '" + name +
                    "'");
            continue;
         }
         
         // OK to delete this user from the group
         // 1. get all groups for the user
         String node = new String("/security/accounts/users/" + delUser);
         String[] groups = ds.getNodeStrings(node, "groups");
         
         if (groups == null)
         {
            // inconsistency found
            message("Directory access failed for " + 
                    "/security/accounts/users/" + delUser);
            ds.closeBatch(false, false);
            return false;
         }
            
         // 2. find the group being serviced
         int j = 0;
         for (j = 0; j < groups.length; j++)
         {
            if (groups[j].compareToIgnoreCase(name) == 0)
            {
               break;
            }
         }
         
         if (j == groups.length)
         {
            // inconsistency found
            message("Directory inconsistency. User '" + delUser +
                    "' should be in group '" + name + "'.");
            ds.closeBatch(false, false);
            return false;
         }
         
         // 3. delete group by index
         if (!ds.deleteNodeAttributeValue(node, "groups", j))
         {
            message("Directory modification failed for " + node); 
            ds.closeBatch(false, false);
            return false;
         }
      }
      
      // close batch with commit and no refresh
      rc = ds.closeBatch(true, false);
      if (!rc)
      {
         message("Error committing directory changes.");
      }
      
      return rc;
   }
   
   /**
    * Synchronizes a directory node attribute with the given value. It means
    * setting or removing the attribute value.
    * 
    * @param    id
    *           object ID for a directory node
    * 
    * @param    attr
    *           node attribute name
    * 
    * @param    value
    *           attribute value to sync with; may be <code>null</code>
    * 
    * @return   <code>true</code> if successful
    */
   private boolean syncString(String id, String attr, String value)
   {
      boolean rc = true;
      
      if (value != null)
      {
         // check the attribute existence
         String old = ds.getNodeString(id, attr);
         
         if (old != null)
         {
            // sync by setting the value
            rc = ds.setNodeString(id, attr, value);
         }
         else
         {
            // sync by adding the value
            rc = ds.addNodeString(id, attr, value);
         }
      }
      else
      {
         // sync by removing the attribute
         if (ds.getNodeString(id, attr) != null)
         {
            rc = ds.deleteNodeAttribute(id, attr);
         }
      }
      
      return rc;
   }
   
   /**
    * Synchronizes a directory node attribute with the given value. It means
    * setting or removing the attribute value.
    * 
    * @param    id
    *           object ID for a directory node
    * 
    * @param    attr
    *           node attribute name
    * 
    * @param    value
    *           attribute value to sync with; may be <code>null</code>
    * 
    * @return   <code>true</code> if successful
    */
   private boolean syncStrings(String id, String attr, String[] value)
   {
      boolean rc = true;
      
      // remove the attribute first
      if (ds.getNodeStrings(id, attr) != null)
      {
         rc = ds.deleteNodeAttribute(id, attr);
      }
         
      if (value != null && value.length > 0)
      {
         // recreate the values
         for (int i = 0; i < value.length; i++)
         {
            rc = ds.addNodeString(id, attr, value[i]);
         }
      }
      
      return rc;
   }
   
   /**
    * Synchronizes a directory node attribute with the given value. It means
    * setting or removing the attribute value.
    * 
    * @param    id
    *           object ID for a directory node
    * 
    * @param    attr
    *           node attribute name
    * 
    * @param    value
    *           attribute value to sync with; may be <code>null</code>
    * 
    * @return   <code>true</code> if successful
    */
   private boolean syncBoolean(String id, String attr, Boolean value)
   {
      boolean rc = true;
      
      if (value != null)
      {
         // check the attribute existence
         Boolean old = ds.getNodeBoolean(id, attr);
         
         if (old != null)
         {
            // sync by setting the value
            rc = ds.setNodeBoolean(id, attr, value);
         }
         else
         {
            // sync by adding the value
            rc = ds.addNodeBoolean(id, attr, value);
         }
      }
      else
      {
         // sync by removing the attribute
         if (ds.getNodeBoolean(id, attr) != null)
         {
            rc = ds.deleteNodeAttribute(id, attr);
         }
      }
      
      return rc;
   }

   /**
    * Synchronizes a directory node attribute with the given value. It means
    * setting or removing the attribute value.
    *
    * @param    id
    *           object ID for a directory node
    *
    * @param    attr
    *           node attribute name
    *
    * @param    value
    *           attribute value to sync with; may be <code>null</code>
    *
    * @return   <code>true</code> if successful
    */
   private boolean syncInteger(String id, String attr, Integer value)
   {
      boolean rc = true;

      if (value != null)
      {
         // check the attribute existence
         Integer old = ds.getNodeInteger(id, attr);

         if (old != null)
         {
            // sync by setting the value
            rc = ds.setNodeInteger(id, attr, value);
         }
         else
         {
            // sync by adding the value
            rc = ds.addNodeInteger(id, attr, value);
         }
      }
      else
      {
         // sync by removing the attribute
         if (ds.getNodeInteger(id, attr) != null)
         {
            rc = ds.deleteNodeAttribute(id, attr);
         }
      }

      return rc;
   }
   
   /**
    * Synchronizes a directory node attribute with the given value. It means
    * setting or removing the attribute value.
    * 
    * @param    id
    *           object ID for a directory node
    * 
    * @param    attr
    *           node attribute name
    * 
    * @param    value
    *           attribute value to sync with; may be <code>null</code>
    * 
    * @return   <code>true</code> if successful
    */
   private boolean syncBytes(String id, String attr, byte[] value)
   {
      boolean rc = true;
      
      if (value != null)
      {
         // check the attribute existence
         byte[] old = ds.getNodeByteArray(id, attr);
         
         if (old != null)
         {
            // sync by setting the value
            rc = ds.setNodeByteArray(id, attr, value);
         }
         else
         {
            // sync by adding the value
            rc = ds.addNodeByteArray(id, attr, value);
         }
      }
      else
      {
         // sync by removing the attribute
         if (ds.getNodeByteArray(id, attr) != null)
         {
            rc = ds.deleteNodeAttribute(id, attr);
         }
      }
      
      return rc;
   }
   
   /**
    * Synchronizes a directory node attribute with the given value. It means
    * setting or removing the attribute value.
    * 
    * @param    id
    *           object ID for a directory node
    * 
    * @param    attr
    *           node attribute name
    * 
    * @param    value
    *           attribute value to sync with; may be <code>null</code>
    * 
    * @return   <code>true</code> if successful
    */
   private boolean syncDate(String id, String attr, DateValue value)
   {
      boolean rc = true;
      
      if (value != null)
      {
         // check the attribute existence
         DateValue old = ds.getNodeDate(id, attr);
         
         if (old != null)
         {
            // sync by setting the value
            rc = ds.setNodeDate(id, attr, value);
         }
         else
         {
            // sync by adding the value
            rc = ds.addNodeDate(id, attr, value);
         }
      }
      else
      {
         // sync by removing the attribute
         if (ds.getNodeDate(id, attr) != null)
         {
            rc = ds.deleteNodeAttribute(id, attr);
         }
      }
      
      return rc;
   }
   
   /**
    * Synchronizes a directory node attribute with the given value. It means
    * setting or removing the attribute value.
    * 
    * @param    id
    *           object ID for a directory node
    * 
    * @param    attr
    *           node attribute name
    * 
    * @param    value
    *           attribute value to sync with; may be <code>null</code>
    * 
    * @return   <code>true</code> if successful
    */
   private boolean syncTime(String id, String attr, TimeValue value)
   {
      boolean rc = true;
      
      if (value != null)
      {
         // check the attribute existence
         TimeValue old = ds.getNodeTime(id, attr);
         
         if (old != null)
         {
            // sync by setting the value
            rc = ds.setNodeTime(id, attr, value);
         }
         else
         {
            // sync by adding the value
            rc = ds.addNodeTime(id, attr, value);
         }
      }
      else
      {
         // sync by removing the attribute
         if (ds.getNodeTime(id, attr) != null)
         {
            rc = ds.deleteNodeAttribute(id, attr);
         }
      }
      
      return rc;
   }
   
   /**
    * Creates a group account directory object.
    * 
    * @param    gid
    *           object ID for the new directory object
    *
    * @param    text
    *           group description
    *
    * @param    mode
    *           authorization mode
    *
    * @param    authPlugin
    *           custom authentication plugin class
    *
    * @return   <code>true</code> if successful
    */
   private boolean createGroupNode(String gid, String text,
                                   int mode, String authPlugin)
   {
      // create directory object
      List<Attribute> data = new ArrayList<Attribute>(4);

      // group data: description (string)
      data.add(new Attribute(ds.getClassNodeAttribute("group", "description"),
                             new Object[]  { text }));

      if (mode != SecurityConstants.AUTH_MODE_NONE)
      {
         data.add(new Attribute(ds.getClassNodeAttribute("group", "mode"),
                                new Object[] { Integer.valueOf(mode) }));
      }

      if (authPlugin != null)
      {
         data.add(new Attribute(ds.getClassNodeAttribute("group",
                                                         "auth-plugin"),
                                new Object[] { authPlugin }));
      }


      return ds.addNode(gid, "group", data.toArray(new Attribute[0]));
   }
   
   /**
    * Creates a user account directory object.
    * 
    * @param    uid
    *           object ID for the new directory object
    * 
    * @param    enabled
    *           enabled/disabled status of the account
    * 
    * @param    person
    *           account owner's name; may be <code>null</code>
    *
    * @param    osuser
    *           the OS user associated with the account
    * 
    * @param    mode
    *           account authentication mode
    * 
    * @param    alias
    *           related certificate's alias; may be <code>null</code>
    * 
    * @param    groups
    *           array of group names this account is a part of; may be
    *           <code>null</code>
    * 
    * @param    protect
    *           password-protected status of the account
    * 
    * @param    password
    *           password hash; may be <code>null</code>
    * 
    * @param    date
    *           date of the last password change; may be <code>null</code>
    * 
    * @param    time
    *           time of the last password change; may be <code>null</code>
    *
    * @param    authPlugin
    *           custom authentication plugin class
    * 
    * @return   <code>true</code> if successful
    */
   private boolean createUserNode(String uid, boolean enabled, String person, String osuser, int mode,
                                  String authPlugin, String alias, String[] groups, boolean protect,
                                  byte[] password, DateValue date, TimeValue time)
   {
      // enumerate present attributes
      int na = 2;                 // for "enabled" and "protected"
      
      if (person != null)
      {
         na++;
      }
      
      if (osuser != null)
      {
         na++;
      }
         
      if (alias != null)
      {
         na++;
      }
      
      if (groups != null && groups.length > 0)
      {
         na++;
      }
      
      if (password != null)
      {
         na++;
      }
      
      if (date != null)
      {
         na++;
      }
      
      if (time != null)
      {
         na++;
      }

      if (mode != SecurityConstants.AUTH_MODE_NONE)
      {
         na++;
      }

      if (authPlugin != null)
      {
         na++;
      }
      
      // create the exact size array
      Attribute[] data = new Attribute[na];
      
      // stuff the array
      int i = 0;
      
      data[i++] = new Attribute(ds.getClassNodeAttribute("user", "enabled"),
                                new Object[] {Boolean.valueOf(enabled)});
      
      data[i++] = new Attribute(ds.getClassNodeAttribute("user", "protected"),
                                new Object[] {Boolean.valueOf(protect)});
      
      if (mode != SecurityConstants.AUTH_MODE_NONE)
      {
         data[i++] = new Attribute(ds.getClassNodeAttribute("user", "mode"),
                                   new Object[] {Integer.valueOf(mode)});
      }

      if (authPlugin != null)
      {
         data[i++] = new Attribute(ds.getClassNodeAttribute("user",
                                                            "auth-plugin"),
                                   new Object[] {authPlugin});
      }
      
      if (person != null)
      {
         data[i++] = new Attribute(
                         ds.getClassNodeAttribute("user", "person"),
                         new Object[] {person});
      }

      if (osuser != null)
      {
         data[i++] = new Attribute(
                         ds.getClassNodeAttribute("user", "osuser"),
                         new Object[] {osuser});
      }
      
      if (alias != null)
      {
         data[i++] = new Attribute(
                         ds.getClassNodeAttribute("user", "alias"),
                         new Object[] {alias});
      }
      
      if (groups != null && groups.length > 0)
      {
         data[i++] = new Attribute(
                         ds.getClassNodeAttribute("user", "groups"),
                         groups);
      }
      
      if (password != null)
      {
         data[i++] = new Attribute(
                         ds.getClassNodeAttribute("user", "password"),
                         new Object[] {password});
      }
      
      if (date != null)
      {
         data[i++] = new Attribute(
                         ds.getClassNodeAttribute("user", "pwsetdate"),
                         new Object[] {date});
      }
      
      if (time != null)
      {
         data[i++] = new Attribute(
                         ds.getClassNodeAttribute("user", "pwsettime"),
                         new Object[] {time});
      }
      
      // create directory object
      return ds.addNode(uid, "user", data);
   }

   /**
    * Creates a OS user account directory object.
    *
    * @param    uid
    *           object ID for the new directory object
    * @param    description
    *           description of the OS user
    * @param    enabled
    *           enabled/disabled status of the account
    * @param    password
    *           password hash; may be <code>null</code>
    * @param    date
    *           date of the last password change; may be <code>null</code>
    * @param    time
    *           time of the last password change; may be <code>null</code>
    * @param    protect
    *           protect the password
    *
    * @return   <code>true</code> if successful
    */
   private boolean createOsUserNode(String uid,
                                    String description,
                                    boolean enabled,
                                    byte[] password,
                                    DateValue date,
                                    TimeValue time)
   {
      // enumerate present attributes
      int na = 1; // for "enabled"

      if (description != null)
      {
         na++;
      }

      if (password != null)
      {
         na++;
      }

      if (date != null)
      {
         na++;
      }

      if (time != null)
      {
         na++;
      }

      // create the exact size array
      Attribute[] data = new Attribute[na];

      int i = 0;
      
      data[i++] = new Attribute(ds.getClassNodeAttribute("osuser", "enabled"),
                                new Object[] {enabled});

      if (description != null)
      {
         data[i++] = new Attribute(
            ds.getClassNodeAttribute("osuser", "description"),
            new Object[] {description});
      }
      
      if (password != null)
      {
         data[i++] = new Attribute(
            ds.getClassNodeAttribute("osuser", "password"),
            new Object[] {password});
      }

      if (date != null)
      {
         data[i++] = new Attribute(
            ds.getClassNodeAttribute("osuser", "pwsetdate"),
            new Object[] {date});
      }

      if (time != null)
      {
         data[i++] = new Attribute(
            ds.getClassNodeAttribute("osuser", "pwsettime"),
            new Object[] {time});
      }

      // create directory object
      return ds.addNode(uid, "osuser", data);
   }
   
   /**
    * Creates a process account directory object.
    * 
    * @param    id
    *           object ID for the new directory object
    *
    * @param    enabled
    *           enabled/disabled status of the account
    *
    * @param    text
    *           description
    *
    * @param    alias
    *           process certificate alias
    *
    * @param    server
    *           <code>true</code> if this process is a server, 
    *           <code>false</code> if an application
    *
    * @param    master
    *           <code>true</code> if this server process is a master server, 
    *           <code>false</code> otherwise
    *
    * @return   <code>true</code> if successful
    */
   private boolean createProcessNode(String id, boolean enabled, String text,  
                                     String alias, boolean server,
                                     boolean master)
   {
      // enumerate present attributes
      int na = 3;                 // for "enabled", "server" and  "master"
      
      if (text != null)
      {
         na++;
      }
      
      if (alias != null)
      {
         na++;
      }
      
      // create the exact size array
      Attribute[] data = new Attribute[na];
      
      // stuff the array
      int i = 0;
      
      data[i++] = new Attribute(ds.getClassNodeAttribute("process", "enabled"),
                                new Object[] {enabled});
      
      data[i++] = new Attribute(ds.getClassNodeAttribute("process", "server"),
                                new Object[] {server});
                                
      data[i++] = new Attribute(ds.getClassNodeAttribute("process", "master"),
                                new Object[] {master});
                                
      if (text != null)
      {
         data[i++] = new Attribute(
                         ds.getClassNodeAttribute("process", "description"),
                         new Object[] {text});
      }
      
      if (alias != null)
      {
         data[i++] = new Attribute(
                         ds.getClassNodeAttribute("process", "alias"),
                         new Object[] {alias});
      }
      
      // create directory object
      return ds.addNode(id, "process", data);
   }
   
   /**
    * Creates a certificate directory object.
    * 
    * @param    id
    *           object ID for the new directory object
    *
    * @param    enc
    *           certificate encoding
    *
    * @return   <code>true</code> if successful
    */
   private boolean createCertNode(String id, byte[] enc)
   {
      // create directory object
      Attribute[] data = new Attribute[]
      {
         // certificate data: encoding (bytes)
         new Attribute(ds.getClassNodeAttribute("bytes", "value"),
                       new Object[]  { enc }), 
      };
      return ds.addNode(id, "bytes", data);
   }
   
   /**
    * Modifies all ACLs that refer to the given subject so that they no longer
    * do. If the subject was the only one in an ACL, the ACL gets removed.
    * 
    * @param    name
    *           subject name to be removed from all ACLs
    *
    * @return   <code>true</code> if successful
    */
   private boolean removeSubjectFromACLs(String name)
   {
      boolean rc = false;
      
      // list all existing ACL instances for all resource types
      String[] paths = enumerateACLInstancePaths();
      
      // process ACL instances
      for (int i = 0; i < paths.length; i++)
      {
         // open editing batch for the instance
         rc = ds.openBatch(paths[i]);
         if (!rc)
         {
            message("Can't lock directory branch " + paths[i]); 
            return false;
         };
         
         // enumerate resource types for this instance
         String[] resources = ds.enumerateNodes(paths[i]);
         
         // process all ACLs for every resource type
         allAcls:
         for (int j = 0; j < resources.length; j++)
         {
            // enumerate ACLs
            String origin = paths[i] + "/" + resources[j];
            String[] acls = ds.enumerateNodes(origin);
            
            // process each ACL node
            for (int k = 0; k < acls.length; k++)
            {
               // path to the ACL container
               String acl = origin + "/" + acls[k];
               
               // read existing subjects
               String[] subjects = ds.getNodeStrings(acl + "/subjects", 
                                                     "values");
               if (subjects == null || subjects.length == 0)
               {
                  continue;
               }
               
               // check if this ACL contains the name
               int l = 0;
               for (l = 0; l < subjects.length; l++)
               {
                  if (name.equalsIgnoreCase(subjects[l]))
                  {
                     break;
                  }
               }
               
               if (l == subjects.length)
               {
                  // name is not part of this ACL
                  continue;
               }
               
               // modify or delete the ACL
               if (subjects.length == 1)
               {
                  // the only subject is the name. delete the ACL
                  rc = Utils.removeDirectoryNode(ds, acl);
                  if (!rc)
                  {
                     message("Can't delete ACL node " + acl);
                     break allAcls;
                  };
               }
               else
               {
                  // delete the name from the subject list, found at index l
                  rc = ds.deleteNodeAttributeValue(acl + "/subjects", "values",
                                                   l);
                  if (!rc)
                  {
                     message("Can't delete attribute value '" + name +  
                             "' from " + acl+ "/subjects");
                     break allAcls;
                  };
               }
            }
         }   
         
         // close batch with commit or rollback and no refresh
         boolean rcc = ds.closeBatch(rc, false);
         if (!rcc)
         {
            message("Error committing directory changes.");
            rc = false;
            break;
         }
      }
      
      return rc;
   }   
      
   /**
    * Modifies all ACLs that refer to the given subject so that they also
    * refer to the clone.
    * 
    * @param    name
    *           subject name to be cloned in all ACLs
    *
    * @param    nameTo
    *           clone name
    * 
    * @return   <code>true</code> if successful
    */
   private boolean cloneSubjectInACLs(String name, String nameTo)
   {
      boolean rc = false;
      
      // list all existing ACL instances for all resource types
      String[] paths = enumerateACLInstancePaths();
      
      // process ACL instances
      for (int i = 0; i < paths.length; i++)
      {
         // open editing batch for the instance
         rc = ds.openBatch(paths[i]);
         if (!rc)
         {
            message("Can't lock directory branch " + paths[i]); 
            return false;
         };
         
         // enumerate resource types for this instance
         String[] resources = ds.enumerateNodes(paths[i]);
         
         // process all ACLs for every resource type
         allAcls:
         for (int j = 0; j < resources.length; j++)
         {
            // enumerate ACLs
            String origin = paths[i] + "/" + resources[j];
            String[] acls = ds.enumerateNodes(origin);
            
            // process each ACL node
            for (int k = 0; k < acls.length; k++)
            {
               // path to the ACL container
               String acl = origin + "/" + acls[k];
               
               // read existing subjects
               String[] subjects = ds.getNodeStrings(acl + "/subjects", 
                                                     "values");
               if (subjects == null || subjects.length == 0)
               {
                  continue;
               }
               
               // check if this ACL contains the name
               int l = 0;
               for (l = 0; l < subjects.length; l++)
               {
                  if (name.equalsIgnoreCase(subjects[l]))
                  {
                     break;
                  }
               }
               
               if (l == subjects.length)
               {
                  // name is not part of this ACL
                  continue;
               }
               
               // add the clone name to the ACL
               rc = ds.addNodeString(acl + "/subjects", "values", nameTo);
               if (!rc)
               {
                  message("Can't add attribute value '" + nameTo +  
                          "' to " + acl+ "/subjects");
                  break allAcls;
               };
            }
         }   
         
         // close batch with commit or rollback and no refresh
         boolean rcc = ds.closeBatch(rc, false);
         if (!rcc)
         {
            message("Error committing directory changes.");
            rc = false;
            break;
         }
      }
      
      return rc;
   }   
   
   /**
    * Enumerates ACL instances defined in the directory. Instances are
    * identified by their directory path. The shared ACL instance has the
    * fixed path <code>/security/acl</code>
    * 
    * @return   array of instance names, which may be empty
    */
   private String[] enumerateACLInstances()
   {
      // container for the results
      List<String> list = new LinkedList<String>();
      
      // check the shared instance first
      if (ds.getNodeClass("/security/acl").equals("/meta/class/container"))
      {
         list.add("");
      }
      
      // enumerate private instances
      String[] priv = ds.enumerateNodes("/security/acls");
      
      if (priv != null)
      {
         for (int i = 0; i < priv.length; i++)
         {
            list.add(priv[i]);
         }
      }
      
      return list.toArray(new String[]{});
   }
   
   /**
    * Enumerates ACL instances defined in the directory, that have ACLs
    * defined for the specified resource type.
    * <p>
    * Instances are identified by their directory path. The shared ACL
    * instance has the fixed path <code>/security/acl</code>
    * 
    * @return   array of instance names, which may be empty
    */
   private String[] enumerateACLInstances(String resourceType)
   {
      // container for the results
      List<String> list = new LinkedList<String>();
      
      // check the shared instance first
      if (ds.getNodeClass("/security/acl").equals("/meta/class/container"))
      {
         list.add("");
      }
      
      // enumerate private instances
      String[] priv = ds.enumerateNodes("/security/acls");
      
      if (priv != null)
      {
         for (int i = 0; i < priv.length; i++)
         {
            list.add(priv[i]);
         }
      }
      
      // filter the list and exclude instances having no resource type
      Iterator<String> il = list.iterator();
      scanList:
      while (il.hasNext())
      {
         // enumerate resource types for this instance
         String instance = il.next();
         String node = instance.equals("") ? "/security/acl" :
                                             "/security/acls/" + instance;
         String[] rtypes = ds.enumerateNodes(node);
         
         // compare types
         if (rtypes != null)
         {
            for (int i = 0; i < rtypes.length; i++)
            {
               if (rtypes[i].equals(resourceType))
               {
                  continue scanList;
               }
            }
         }
         
         // no match found; delete this instance from the list
         il.remove();
      }
      
      return list.toArray(new String[]{});
   }
   
   /**
    * Enumerates ACL instances defined in the directory. Instances are
    * identified by their directory path. The shared ACL instance has the
    * fixed path <code>/security/acl</code>
    * 
    * @return   array of directory paths, which may be empty
    */
   private String[] enumerateACLInstancePaths()
   {
      String[] names = enumerateACLInstances();
      if (names == null)
      {
         return new String[0];
      }
      
      String[] paths = new String[names.length];
      for (int i = 0; i < names.length; i++)
      {
         paths[i] = names[i].length() == 0 ?
                    "/security/acl" : "/security/acls/" + names[i];
      }
      
      return paths;
   }
   
   /**
    * Enumerates ACLs under the given directory path and combines then into
    * the given map.
    * 
    * @param    path
    *           directory branch to process
    * 
    * @param    comb
    *           a map to receive the IDs to object paths mappings
    * 
    * @param    badIds
    *           starting counter of bad IDs that should be incremented
    * 
    * @return   new count of bad object IDs
    */
   private int enumerateACLs(String path, Map<Integer, String> comb, int badIds)
   {
      // enumerate children
      String[] acls = ds.enumerateNodes(path);
      if (acls == null || acls.length == 0)
      {
         return badIds;
      }
      
      // put every ACL into the map
      for (int i = 0; i < acls.length; i++)
      {
         int k = 0;
         
         try
         {
            // get the numeric ID
            k = Integer.parseInt(acls[i]);
         }
         catch (NumberFormatException e)
         {
            // assign negative IDs to the misformed oids
            badIds++;
            k = -badIds;
         }
         
         comb.put(k, path + "/" + acls[i]);
      }
      
      return badIds;
   }
   
   /**
    * Creates an ACL definition from the ACLs for the specified resource type.
    * Gets the ACLs for the named instance only, if requested. Otherwise
    * combines the ACLs of the shared and private instances. Creates the oid
    * to the directory object map for all found ACLs.
    * 
    * @param    resourceType
    *           name of the resource of interest
    * 
    * 
    * @param    instanceName
    *           name of the ACL instance or <code>null</code> meaning the
    *           effective combination is requested
    * 
    * @param    mpath
    *           mandatory directory path to the resource container; can't be
    *           <code>null</code>
    * 
    * @param    opath
    *           optional directory path to the resource container; must be
    *           <code>null</code> if instanceNamwe is given; otherwise must be
    *           specified
    * 
    * @return   ACL definition
    */
   private AclDef getAclWorker(String resourceType, String instanceName, 
                               String mpath, String opath)
   {
      // validate the resource type
      int ord = checkResourceType(resourceType);
      if (ord == -1)
      {
         return null;
      }
      
      // validate other arguments
      if (mpath == null)
      {
         message("internal error 1 while getting ACLs");
         return null;
      }
      
      if (instanceName == null && opath == null)
      {
         message("internal error 2 while getting ACLs");
         return null;
      }
      
      if (instanceName != null && opath != null)
      {
         message("internal error 3 while getting ACLs");
         return null;
      }
      
      // instance of AclDef
      AclDef ad       = new AclDef();
      ad.resourceName = resourceType;
      ad.server       = instanceName;
      ad.subjects     = new HashMap<Integer, String>();
      ad.errors       = 0;
      
      // temporary reverse mapping of subject IDs into integers
      Map<String, Integer> rmap = new HashMap<String, Integer>();
      
      // create a combination map: Integer oid to ACL path
      SortedMap<Integer, String> amap = new TreeMap<Integer, String>();
      
      // enumerate ACLs under the mandatory path
      int badIds = enumerateACLs(mpath, amap, 0);
      
      // enumerate ACLs under the optional path
      if (opath != null)
      {
         badIds = enumerateACLs(opath, amap, badIds);
      }
      
      // quick return if nothing to do
      int numAcls = amap.size();
      if (numAcls == 0)
      {
         ad.acls = new Acl[] {};
         return ad;
      }

      // there are ACLs
      ad.acls = new Acl[numAcls];
      
      // read all ACLs and create records
      // processing ordered sequence of ACLs
      Set<Integer> set = amap.keySet();
      Iterator<Integer> iter = set.iterator();
      
      int i = 0;
      while (iter.hasNext())
      {
         ad.acls[i] = new Acl();
         ad.acls[i].errOid = false;
         
         // get the next ACL and parse components
         Integer k = iter.next();
         String base = amap.get(k);
         String oid = base.substring(base.lastIndexOf("/") + 1);
         String ori = null;
         if (base.startsWith("/security/acl/"))
         {
            ori = "";
         }
         else
         {
            ori = base.substring(15);
            ori = ori.substring(0, ori.indexOf("/"));
         }
                      
         // conditionally create the map for this ACL instance & resource type
         mapTypes = mapInst.get(ori);
         if (mapTypes == null)
         {
            mapTypes = new HashMap<String, Map<Integer, String>>();
            mapInst.put(ori, mapTypes);
         }
         
         mapId = mapTypes.get(resourceType);
         if (mapId == null)
         {
            mapId = new HashMap<Integer, String>();
            mapTypes.put(resourceType, mapId);
         }
         
         // flag the bad oids
         if (k.intValue() < 0)
         {
            ad.acls[i].errOid = true;
            message(String.format(
                    "ACL number %d in <%s><%s> has bad ID <%s>", k.intValue(),
                     resourceType, instanceName, oid));
         }
         
         // store the numeric oid to node oid mapping             
         mapId.put(k, oid);
         
         ad.acls[i].origin = instanceName == null ? ori : null;
         ad.acls[i].oid = k.intValue();
         
         // read resource info
         ad.acls[i].name = ds.getNodeString(base + "/resource-instance",
                                            "reference");
         if (ad.acls[i].name == null)
         {
            ad.acls[i].errName = true;
            ad.acls[i].name = "*** error ***";
         }
         else
         {
            ad.acls[i].errName = false;
         }
         
         Boolean xact = ds.getNodeBoolean(base + "/resource-instance",
                                          "reftype");
         if (xact == null)
         {
            ad.acls[i].errExact = true;
            ad.acls[i].exact = true;
         }
         else
         {
            ad.acls[i].errExact = false;
            ad.acls[i].exact = xact.booleanValue();
         }
         
         // read and instantiate rights
         Rights rights = sc.createRights(ds, ord, base + "/rights");
         if (rights == null)
         {
            ad.acls[i].errRights = true;
            ad.acls[i].rights = null;
         }
         else
         {
            ad.acls[i].errRights = false;
            ad.acls[i].rights = rights;
         }
            
         // read and map subjects
         String[] subjs = ds.getNodeStrings(base + "/subjects", "values");
         if (subjs == null || subjs.length == 0)
         {
            ad.acls[i].errSubjs = true;
            subjs = new String[] {};
         }
         else
         {
            ad.acls[i].errSubjs = false;
         }
         
         ad.acls[i].subjects = new int[subjs.length];
         
         for (int j = 0; j < subjs.length; j++)
         {
            // all_others case
            if (subjs[j].equalsIgnoreCase("all_others"))
            {
               ad.acls[i].subjects[j] = AclDef.ALL_OTHERS;
            }
            else
            {
               Integer n = rmap.get(subjs[j]);
               if (n == null)
               {
                  n = rmap.size();
                  rmap.put(subjs[j], n);
                  ad.subjects.put(n, subjs[j]);
               }
               
               ad.acls[i].subjects[j] = n.intValue();
            }
         }
         
         // keep track of the problematic ACLs
         if (ad.acls[i].errOid   || ad.acls[i].errName  ||  
             ad.acls[i].errExact || ad.acls[i].errSubjs ||
             ad.acls[i].errRights)
         {
            ad.errors++;
         }
         
         i++;
      }
      
      if (ad.errors > 0)
      {
         message(String.format(
                 "Found %d ACLs with various errors in <%s><%s>", ad.errors,
                 resourceType, instanceName));
      }

      
      return ad;
   }
   
   /**
    * Calculates the common parent node for two given nodes.
    * 
    * @param    snode
    *           a directory node
    * 
    * @param    tnode
    *           another directory node
    * 
    * @return   common parent node, which may be just the root node "/".
    */
   private String commonParent(String snode, String tnode)
   {
      String n1 = snode.toLowerCase();
      String n2 = tnode.toLowerCase();
 
      // quick path
      if (n1.equals(n2))
      {
         return n1;
      }
      
      // find the common part character-wise
      int i = 0;
      for (i = 0; i < Math.min(n1.length(), n2.length()); i++)
      {
         if (n1.charAt(i) != n2.charAt(i))
         {
            break;
         }
      }

      String cp = n1.substring(0, i);
      cp = cp.substring(0, cp.lastIndexOf("/"));
      
      return cp;
   }
      
   /**
    * Utility method to store server messages for later retrieval by clients.
    * 
    * @param    msg
    *           array of strings that constitutes a message
    */
   private void message(String[] msg)
   {
      AdminServerImpl.message(msg);
   }
   
   /**
    * Utility method to store server messages for later retrieval by clients.
    * 
    * @param    msg
    *           a single string that constitutes a message
    */
   private void message(String msg)
   {
      AdminServerImpl.message(new String[] {msg});
   }

   /**
    * Synchronizes authentication-related directory node attributes with the
    * given values. It means setting or removing the attribute values.
    *
    * @param  node
    *         Object ID for a directory node.
    * @param  authMode
    *         Authentication mode to set.
    * @param  authPlugin
    *         Authentication plugin to set.
    *
    * @return <code>true</code> if successful 
    */
   private boolean syncAuthSettings(String node,
                                    int authMode,
                                    String authPlugin)
   {
      // do not keep the auth mode if it is AUTH_MODE_NONE
      boolean rc = syncInteger(node, "mode",
              authMode != SecurityConstants.AUTH_MODE_NONE ?
              authMode :
              null);

      if (rc)
      {
         // do not keep the auth plugin if auth mode != AUTH_MODE_CUSTOM
         rc = syncString(node, "auth-plugin",
                         authMode == SecurityConstants.AUTH_MODE_CUSTOM ?
                         authPlugin:
                         null);
      }

      return rc;
   }
   
   /**
    * Utility method to get the custom server extension interface.
    * 
    * @return   the custom server extension interface or <code>null</code>
    */
   private AdminAccountExtension getExtension()
   {
      String customExt = sm.getCustomServerExt();
      if (customExt == null)
      {
         return null;
      }
      
      Class<?> cext = null;
      try
      {
         cext = Class.forName(customExt);
      }
      catch (ClassNotFoundException cnfe)
      {
         return null;
      }
      
      Method mget = null;
      try
      {
         mget = cext.getMethod("getInterface", (Class[])null);
      }
      catch (Exception e)
      {
         return null;
      }
      
      AdminAccountExtension aae = null;
      try
      {
         aae = (AdminAccountExtension)mget.invoke(null, (Object[])null);
      }
      catch (Exception e)
      {
         return null;
      }
      
      return aae;
   }

   /**
    * Admin permissions check routine.
    * 
    * @return   <code>true</code> if any admin permission is assigned
    */
   private boolean checkPermission()
   {
      boolean perm = false;
      
      if (ar != null)
      {
         // check the access rights with the admin plugin first
         perm = ar.checkAnyAdminAccess();
      }
      else
      {
         // fall back to the system resource
         perm = sr.checkAdminAccess();
      }
      
      if (!perm)
      {
         message("insufficient access rights for admin");
      }
      
      return perm;
   }
   
   /**
    * Admin permission check routine for a specific resource instance, leaf
    * node and permission.
    * 
    * @param    name
    *           resource instance name
    * 
    * @param    mode
    *           access modes (requested permission)
    * 
    * @return   <code>true</code> if access granted to any permission
    */
   private boolean checkPermissions(String name, int[] mode)
   {
      boolean perm = false;
      
      if (ar != null)
      {
         // check the access rights with the admin plugin first
         for (int i = 0; i < mode.length; i++)
         {             
            perm |= ar.checkAdminAccess(name, mode[i]) > 0;
            if (perm)
            {
               break;
            }
         }
      }
      else
      {
         // fall back to the system resource
         perm = sr.checkAdminAccess();
      }
      
      if (!perm)
      {
         StringBuilder sb = new StringBuilder();
         sb.append("insufficient access rights for ");
         sb.append(name);
         sb.append(" (");
         for (int i = 0; i < mode.length; i++)
         {             
            sb.append(Integer.toString(mode[i]));
            sb.append(",");
         }
         sb.setLength(sb.length() -1);
         sb.append(" )");
         
         message(sb.toString());
      }
      
      return perm;
   }
   
   /**
    * Admin permission check routine for a specific resource instance, leaf
    * node and permission.
    * 
    * @param    name
    *           resource instance name
    * 
    * @param    mode
    *           access mode (requested permission)
    * 
    * @return   <code>true</code> if access granted
    */
   private boolean checkPermission(String name, int mode)
   {
      boolean perm = queryPermission(name, mode);
      
      if (!perm)
      {
         message(String.format("insufficient access rights for %s (%d)", 
                               name, mode));
      }
      
      return perm;
   }
   
   /**
    * Admin permission check routine for a specific resource instance, leaf
    * node and permission.
    * 
    * @param    name
    *           resource instance name
    * 
    * @param    mode
    *           access mode (requested permission)
    * 
    * @return   <code>true</code> if access granted
    */
   private boolean queryPermission(String name, int mode)
   {
      boolean perm = false;
      
      if (ar != null)
      {
         // check the access rights with the admin plugin first
         perm = ar.checkAdminAccess(name, mode) > 0;
      }
      else
      {
         // fall back to the system resource
         perm = sr.checkAdminAccess();
      }
      
      return perm;
   }
   
   /**
    * Modifies all ACLs in the specified ACL instance which refer to a given
    * subject ID. The modification consists of the deletions and the additions
    * of the subject IDs. Both operations are conditional - they don't fail if
    * the state is already final.
    * 
    * @param    ds
    *           instance of DirectoryService to use (should be bound)
    * 
    * @param    origin
    *           directory path to the ACLs to process, f.i.
    *           <code>/security/acl/application-specific-acls</code>
    * 
    * @param    name
    *           subject name which works as a selector; may be
    *           <code>null</code> if no selection needed
    * 
    * @param    namesDel
    *           array of subject IDs to delete conditionally; may be
    *           <code>null</code>
    * 
    * @param    namesAdd
    *           array of subject IDs to add conditionally; may be
    *           <code>null</code>
    * 
    * @return   <code>true</code> if successful
    */
   private static boolean modifySubjectsInACLs(DirectoryService ds,
                                               String origin,
                                               String   name, 
                                               String[] namesDel,
                                               String[] namesAdd)
   {
      boolean rc = false;
         
      // enumerate ACLs
      String[] acls = ds.enumerateNodes(origin);
      
      // sort the ACLs
      Arrays.sort(acls);
      
      // open editing batch
      rc = ds.openBatch(origin);
      if (!rc)
      {
         LOG.severe("Can't lock directory branch"); 
         return false;
      };
      
      System.out.println("Processing " + origin);
      
      System.out.print("Selector:  ");
      if (name == null)
      {
         System.out.println("none");
      }
      else
      {
         System.out.println("<" + name + ">");
      }
      
      System.out.print("Deletions: ");
      if (namesDel == null)
      {
         System.out.println("none");
      }
      else
      {
         for (int i = 0; i < namesDel.length; i++)
         {
         System.out.print("<" + namesDel[i] + "> ");
         }
         System.out.println();
      }
      
      System.out.print("Additions: ");
      if (namesAdd == null)
      {
         System.out.println("none");
      }
      else
      {
         for (int i = 0; i < namesAdd.length; i++)
         {
         System.out.print("<" + namesAdd[i] + "> ");
         }
         System.out.println();
      }

      System.out.println();
      
      int err = 0;
      
      // process each ACL node
      for (int k = 0; k < acls.length; k++)
      {
         // path to the ACL container
         String acl = origin + "/" + acls[k];
         
         // read existing subjects
         String[] subjects = ds.getNodeStrings(acl + "/subjects", "values");
         if (subjects == null || subjects.length == 0)
         {
            LOG.finer("Skipping (no subjects)" + acls[k]);
            continue;
         }

         List<String> subjectList = Arrays.asList(subjects);
         
         // check if this ACL contains the selector name
         if (name != null)
         {
            boolean ndx = subjectList.contains(name);
            if (!ndx)
            {
               // name is not part of this ACL
               LOG.finer("Skipping " + acls[k]);
               continue;
            }
         }

         LOG.finer("Editing  " + acls[k]);
         String nodeId = acl + "/subjects";
         
         // process deletions
         if (namesDel != null)
         {
            for (int m = 0; m < namesDel.length; m++)
            {
               int ndx = subjectList.indexOf(namesDel[m]);
               if (ndx == -1)
               {
                  System.out.println("   no  " + namesDel[m]);
                  continue;
               }

               rc = ds.deleteNodeAttributeValue(nodeId, "values", ndx);
               if (rc)
               {
                  LOG.finer("   del " + namesDel[m]);
               }
               else
               {
                  LOG.finer("   err del " + namesDel[m]);
                  err++;
               }
            }
         }
         
         // process additions
         if (namesAdd != null)
         {
            for (int m = 0; m < namesAdd.length; m++)
            {
               int ndx = subjectList.indexOf(namesAdd[m]);
               if (ndx >= 0)
               {
                  LOG.finer("   has  " + namesAdd[m]);
                  continue;
               }

               rc = ds.addNodeString(nodeId, "values", namesAdd[m]);
               if (rc)
               {
                  LOG.finer("   add " + namesAdd[m]);
               }
               else
               {
                  LOG.finer("   err add " + namesAdd[m]);
                  err++;
               }
            }
         }
      }
         
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(true, false);
      if (!rcc)
      {
         // Error unlocking directory branch
         rc = false;
      }
      
      return rc;
   }
   
   /**
    * ACL subjects filtering and editing utility.
    * 
    * @param   args
    *          args[0] - the directory file to process
    *          args[1] - the path to the ACLs
    *          args[2] - the selector name in ""
    *          args[3] - the names being deleted in ""
    *          args[4] - the names being added in ""
    */
   private static void subjects(String[] args)
   throws ConfigurationException
   {
      // process arguments
      if (args.length != 5)
      {
         String[] staticText =
            {
               "The correct syntax is:",
               "",
               " java SecurityAdmin subjects dir-file dir-path" +
                  " quoted-selector quoted-deletions" +
                  " quoted-additions",
               "",
               "Use double quotes to specify no selection or more" +
                  " than one name in the deletions and/or additions",
               ""
            };
         LOG.info(StringHelper.arrayToString(staticText));
         return;
      }
      
      // get the selector
      String selector = null;
      if (args[2].length() > 0)
      {
         selector = args[2];
      }
      
      // get the deletions
      String[] dels = null;
      
      if (args[3].length() > 0)
      {
         dels = args[3].trim().split("(\\p{Space})+");
         if (dels.length == 0)
         {
            dels = null;
         }
      }
      
      // get the additions
      String[] adds = null;
      if (args[4].length() > 0)
      {
         adds = args[4].trim().split("(\\p{Space})+");
         if (adds.length == 0)
         {
            adds = null;
         }
      }
      
      // Empty BootstrapConfig
      BootstrapConfig bc = new BootstrapConfig(null, null, null, null);
      
      // directory package configuration
      bc.setServer(true);
      bc.setConfigItem("directory", "backend", "type",     "xml");
      bc.setConfigItem("directory", "xml",     "filename", args[0]);
       
      DirectoryService ds = DirectoryService.createInstance(bc);
      boolean bound = ds.bind();
      if (!bound)
      {
         LOG.severe("Can't bind to the directory");
         return;
      }
      LOG.info("Bound to " + args[0]);
      
      // this utility changes the application-specific-acls ACLs
      // - deletes references to "batch" in subjects
      boolean rc = modifySubjectsInACLs(ds, args[1], selector, dels, adds);
    
      if (rc)
      {
         LOG.info("Success.");
      }
      else
      {
         LOG.info("There were some errors.");
      }
      
      ds.unbind();
   }
   
   /**
    * ACL migration utility.
    * 
    * @param   args
    *          args[0] - the directory file to process
    *          args[1] - the path to the ACLs
    *          args[2] - the class name for the resource plugin
    */
   private static void migrate(String[] args)
   throws ConfigurationException,
          NoSuchMethodException
   {
      // process arguments
      if (args.length != 3)
      {
         LOG.info("The correct syntax is:");
         LOG.info("");
         LOG.info(" java SecurityAdmin migrate dir-file dir-path resource-plugin");   
         LOG.info("");
         return;
      }
      
      // Empty BootstrapConfig
      BootstrapConfig bc = new BootstrapConfig(null, null, null, null);
      
      // directory package configuration
      bc.setServer(true);
      bc.setConfigItem("directory", "backend", "type",     "xml");
      bc.setConfigItem("directory", "xml",     "filename", args[0]);
       
      DirectoryService ds = DirectoryService.createInstance(bc);
      boolean bound = ds.bind();
      if (!bound)
      {
         LOG.info("Can't bind to the directory");
         return;
      }
      LOG.info("Bound to " + args[0]);
      
      // mimic a server environment with SecurityManager and SecurityCache
      // all accounts are read from the directory file
      bc.setServer(false);
      SecurityManager.createInstance(bc);
      
      List<String[]> msgBuf = new LinkedList<String[]>(); 
      SecurityCache sc = new SecurityCache(SecurityCache.LM_DUAL, msgBuf,
                                           new String[] {args[2]});
      printMessages(msgBuf);
      msgBuf = null;
      
      bc.setServer(true);
      
      // this utility migrates the old style ACLs
      String[] seqno = ds.enumerateNodes(args[1]);
      if (seqno == null)
      {
         LOG.info("No input. Either the directory or the path is invalid");
         ds.unbind();
         return;
      }
      
      int ord = 0;
      
      boolean rc = sc.migrateACLs(ds, args[1], seqno, ord);
    
      if (rc)
      {
         LOG.info("Success.");
      }
      else
      {
         LOG.info("There were some errors.");
      }
      
      ds.unbind();
   }
   
   /**
    * ACL subjects list verification utility.
    * 
    * @param   args
    *          args[0] - the directory file to process
    *          args[1] - the path to the ACLs
    *          args[2] - the class name for the resource plugin
    */
   private static void johndoe(String[] args)
   throws ConfigurationException,
          NoSuchMethodException
   {
      // process arguments
      if (args.length != 3)
      {
         LOG.info("The correct syntax is:");
         LOG.info("");
         LOG.info(" java SecurityAdmin johndoe dir-file dir-path resource-plugin");   
         LOG.info("");
         return;
      }
      
      // Empty BootstrapConfig
      BootstrapConfig bc = new BootstrapConfig(null, null, null, null);
      
      // directory package configuration
      bc.setServer(true);
      bc.setConfigItem("directory", "backend", "type",     "xml");
      bc.setConfigItem("directory", "xml",     "filename", args[0]);
       
      DirectoryService ds = DirectoryService.createInstance(bc);
      boolean bound = ds.bind();
      if (!bound)
      {
         LOG.info("Can't bind to the directory");
         return;
      }
      LOG.info("Bound to " + args[0]);
      
      // mimic a server environment with SecurityManager and SecurityCache
      // all accounts are read from the directory file
      bc.setServer(false);
      SecurityManager.createInstance(bc);
      
      List<String[]> msgBuf = new LinkedList<String[]>(); 
      SecurityCache sc = new SecurityCache(SecurityCache.LM_DUAL, msgBuf,
                                           new String[] {args[2]});
      printMessages(msgBuf);
      msgBuf = null;
      
      bc.setServer(true);
      
      // enumerate all ACLs
      String[] seqno = ds.enumerateNodes(args[1]);
      if (seqno == null)
      {
         LOG.info("No input. Either the directory or the path is invalid");
         ds.unbind();
         return;
      }
      
      Arrays.sort(seqno);
      
      int errors = 0;
      
      // loop over all ACLs
      for (int i = 0; i < seqno.length; i++)
      {
         String aclnode = args[1] + "/" + seqno[i];
         String node = aclnode + "/subjects";
         if (ds.getNodeClass(node) == null)
         {
            LOG.info("node " + aclnode + " has no subjects subnode");
            errors++;
            continue;
         }
         
         String[] subjs = ds.getNodeStrings(node, "values");
         if (subjs == null || subjs.length == 0)
         {
            LOG.info("node " + aclnode + " has empty subjects list");
            errors++;
            continue;
         }
         
         for (int n = 0; n < subjs.length; n++)
         {
            if (subjs[n].equalsIgnoreCase("all_others"))
            {
               if (subjs.length != 1)
               {
                  LOG.info("node " + node + " refers to all_others mixed with " +
                      "accounts (warning)");
               }
               continue;
            }
            
            if (sc.getAccountById(subjs[n]) == null)
            {
               LOG.info("node " + node + " refers to invalid account " + subjs[n]);
               errors++;
               continue;
            }
         }
         
         LOG.info("node " + node + " ok");
      }
    
      if (errors == 0)
      {
         LOG.info("Success.");
      }
      else
      {
         LOG.info(String.format("Found %d errors.", errors));
      }
      
      ds.unbind();
   }
   
   /**
    * ACL resource name verification utility.
    * 
    * @param   args
    *          args[0] - the directory file to process
    *          args[1] - the path to the ACLs
    *          args[2] - the class name for the resource plugin
    */
   private static void resource(String[] args)
   throws ConfigurationException,
          NoSuchMethodException
   {
      // process arguments
      if (args.length != 3)
      {
         LOG.info("The correct syntax is:");
         LOG.info("");
         LOG.info(" java SecurityAdmin resource dir-file dir-path resource-plugin");   
         LOG.info("");
         return;
      }
      
      // Empty BootstrapConfig
      BootstrapConfig bc = new BootstrapConfig(null, null, null, null);
      
      // directory package configuration
      bc.setServer(true);
      bc.setConfigItem("directory", "backend", "type",     "xml");
      bc.setConfigItem("directory", "xml",     "filename", args[0]);
       
      DirectoryService ds = DirectoryService.createInstance(bc);
      boolean bound = ds.bind();
      if (!bound)
      {
         LOG.info("Can't bind to the directory");
         return;
      }
      LOG.info("Bound to " + args[0]);
      
      // mimic a server environment with SecurityManager and SecurityCache
      // all accounts are read from the directory file
      bc.setServer(false);
      SecurityManager.createInstance(bc);
      
      List<String[]> msgBuf = new LinkedList<String[]>(); 
      SecurityCache sc = new SecurityCache(SecurityCache.LM_DUAL, msgBuf,
                                           new String[] {args[2]});
      printMessages(msgBuf);
      msgBuf = null;
      
      bc.setServer(true);
      
      // enumerate all ACLs
      String[] seqno = ds.enumerateNodes(args[1]);
      if (seqno == null)
      {
         LOG.info("No input. Either the directory or the path is invalid");
         ds.unbind();
         return;
      }
      
      Arrays.sort(seqno);
      
      int ord = 0;
      // locate the plugin responsible for this resource type
      AbstractResource plugin = sc.getRegistryByOrd(ord).getPlugin();
      
      int errors = 0;
      
      // loop over all ACLs
      for (int i = 0; i < seqno.length; i++)
      {
         String aclnode = args[1] + "/" + seqno[i];
         String node = aclnode + "/resource-instance";
         if (ds.getNodeClass(node) == null)
         {
            LOG.info("node " + aclnode + " has no resource name");
            errors++;
            continue;
         }
         
         String  reference = ds.getNodeString(node, "reference");
         Boolean reftype = ds.getNodeBoolean(node, "reftype");
         
         if (reference == null)
         {
            LOG.info("node " + aclnode + " has no resource reference");
            errors++;
            continue;
         }
         if (reftype == null)
         {
            LOG.info("node " + aclnode + " has no resource reference type");
            errors++;
            continue;
         }
         
         // for exact match instances, verify the instance name
         if (reftype.booleanValue())
         {
            if (!plugin.isInstanceNameValid(reference))
            {
               LOG.info("node " + aclnode + " refers to invalid resource name <" +
                   reference + ">");
               errors++;
               continue;
            }
         }
         
         LOG.info("node " + node + " ok");
      }
    
      if (errors == 0)
      {
         LOG.info("Success.");
      }
      else
      {
         LOG.info(String.format("Found %d errors.", errors));
      }
      
      ds.unbind();
   }
   
   /**
    * Modifies all ACLs in the specified ACL instance which refer to a given
    * subject ID. The modification consists of the deletions and the additions
    * of the subject IDs. Both operations are conditional - they don't fail if
    * the state is already final.
    * 
    * @param    ds
    *           instance of DirectoryService to use (should be bound)
    * 
    * @param    attrName
    *           attribute name to take the plain text password from
    * 
    * @return   <code>true</code> if successful
    */
   private static boolean encodeWorker(DirectoryService ds, String attrName) 
   {
      boolean rc = false;
         
      // enumerate ACLs
      String origin = "/security/accounts/users";
      String[] accs = ds.enumerateNodes(origin);
      
      // sort the ACLs
      Arrays.sort(accs);
      
      // open editing batch
      rc = ds.openBatch(origin);
      if (!rc)
      {
         LOG.warning("Can't lock directory branch"); 
         return false;
      };
      
      LOG.info("Processing " + origin);
      
      LOG.info("Password input: ");
      LOG.info("<" + attrName + ">");
      
      int err = 0;
      
      // process each user account
      for (int k = 0; k < accs.length; k++)
      {
         // path to the account
         String acc = origin + "/" + accs[k];
         
         // read plain password
         String pw = ds.getNodeString(acc, attrName);
         if (pw == null || pw.length() == 0)
         {
            LOG.info("Skipping (no password)" + accs[k]);
            continue;
         }
         
         // encoding password
         byte[] hash = HashPassword.hashPassword(pw);

         // add or set the password hash
         
         LOG.info("Editing  " + accs[k]);

         if (ds.getNodeByteArray(acc, "password") == null)
         {
            rc = ds.addNodeByteArray(acc, "password", hash);
            if (rc)
            {
               LOG.finer("   add");
            }
            else
            {
               LOG.finer("   err add");
               err++;
            }
         }
         else
         {
            rc = ds.setNodeByteArray(acc, "password", 0, hash);
            if (rc)
            {
               LOG.finer("   set");
            }
            else
            {
               LOG.finer("   err set");
               err++;
            }
         }
      }
         
      // close batch with commit or rollback and no refresh
      boolean rcc = ds.closeBatch(true, false);
      if (!rcc)
      {
         // Error unlocking directory branch
         rc = false;
      }
      
      return rc;
   }
   
   /**
    * User password encoding utility.
    * 
    * @param   args
    *          args[0] - the directory file to process
    *          args[1] - the attribute name to take plain password from
    */
   private static void encode(String[] args)
   throws ConfigurationException
   {
      // process arguments
      if (args.length != 2)
      {
         LOG.info("The correct syntax is:");
         LOG.info("");
         LOG.info(" java SecurityAdmin encode dir-file attr-name"); 
         LOG.info("");
         return;
      }
      
      // Empty BootstrapConfig
      BootstrapConfig bc = new BootstrapConfig(null, null, null, null);
      
      // directory package configuration
      bc.setServer(true);
      bc.setConfigItem("directory", "backend", "type",     "xml");
      bc.setConfigItem("directory", "xml",     "filename", args[0]);
       
      DirectoryService ds = DirectoryService.createInstance(bc);
      boolean bound = ds.bind();
      if (!bound)
      {
         LOG.severe("Can't bind to the directory");
         return;
      }
      LOG.info("Bound to " + args[0]);
      
      // this utility encodes passwords
      boolean rc = encodeWorker(ds, args[1]);
    
      if (rc)
      {
         LOG.info("Success.");
      }
      else
      {
         LOG.info("There were some errors.");
      }
      
      ds.unbind();
   }
   
   /**
    * Message buffer printout
    * 
    * @param   msgBuf
    *          Message buffer after SecurityCache
    */
   private static void printMessages(List<String[]> msgBuf)
   {
      msgBuf.stream()
            .flatMap(Arrays::stream)
            .forEachOrdered(LOG::info);
   }

   /**
    * Application entry point. Since more than one operation cab be performed,
    * the operation has to be specified with the first argument.
    * 
    * @param   args
    *          Command line parameters. The first one specifies the utility
    *          and the rest is utility specific. The currently implemented
    *          utilities are:
    *          <ul>
    *             <li>subjects - filters the ACLs by a subject and edits the
    *                            list of subjects with deletions and additions
    *             <li>migrate - migrates the ACLs from the old to the new style
    *             <li>johndoe - verifies the accounts validity in ACLs
    *             <li>resource - verifies the resource names in ACLs
    *             <li>encode - encodes passwords in user accounts
    *          </ul>
    */
   public static void main(String[] args)
   {
      // inspect the first argument and identify the operation
      int argn = args.length;
      int oper = 0;
      boolean fail = false;
      
      if (argn == 0)
      {
         fail = true;
      }
      else
      {
         if (args[0].equalsIgnoreCase("subjects"))
         {
            oper = 1;
         }
         else if (args[0].equalsIgnoreCase("migrate"))
         {
            oper = 2;
         }
         else if (args[0].equalsIgnoreCase("johndoe"))
         {
            oper = 3;
         }
         else if (args[0].equalsIgnoreCase("resource"))
         {
            oper = 4;
         }
         else if (args[0].equalsIgnoreCase("encode"))
         {
            oper = 5;
         }
         else
         {
            fail = true;
         }
      }
      
      if (fail)
      { 
         String[] help = 
         {
            "A valid operation should be specified",
            "Valid operations are:",
            "   subjects",
            "   migrate",
            "   jondoe",
            "   resource",
            "   encode",
            "Type  <operation ?> for more help"
         };
         LOG.info(StringHelper.arrayToString(help));
         
         System.exit(1);
      }
      
      // prepare the parameters for the operation
      argn--;
      String[] pars = new String[argn];
      System.arraycopy(args, 1, pars, 0, argn);
      
      // invoke the utility
      try
      {
         switch(oper)
         {
            case 1:
               subjects(pars);
               break;
               
            case 2:
               migrate(pars);
               break;

            case 3:
               johndoe(pars);
               break;

            case 4:
               resource(pars);
               break;

            case 5:
               encode(pars);
               break;
         }
      }
      catch (Exception e)
      {
         LOG.severe("", e);
      }
   }
}