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