# SecurityManager.java

    /*
    ** Module   : SecurityManager.java
    ** Abstract : security kernel
    **
    ** Copyright (c) 2005-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- --------------------------------- Description ---------------------------------
    ** 001 NVS 20050208   @19692 Created initial implementation.
    ** 002 NVS 20050412   @20687 Variables and functions are supported through
    **                           the library interface. Library methods have
    **                           easy access to Rights instance.
    ** 003 NVS 20050414   @20714 Debug output methods are made public to be
    **                           accessible from resource plugins.
    ** 004 NVS 20050421   @20871 Passing a generic Object between compute and
    **                           evaluate calls and the resource plugin's
    **                           library of exported variables and methods
    **                           instead of Rights. This allows arbitrary
    **                           plugin's object to serve as a convenient
    **                           data linkage.
    ** 005 NVS 20050425   @20872 SecurityManager no longer mediates the batch
    **                           editing sessions. Applications call directory
    **                           service directly to do directory editing.
    **                           Directory service uses these notification
    **                           methods of SecurityManager to signal batch
    **                           related events: openBatch(), isEditing() and
    **                           closeBatch().
    ** 006 NVS 20050427   @20913 Enhanced to provide unlimited storage for any
    **                           kind of tokens (objects) that can be associa-
    **                           ted with this context. The need is to get rid
    **                           of all sorts of per thread state information
    **                           and replace it with per context one. This
    **                           overcomes the problem of arbitrary chosen
    **                           threads from the thread pool to serve eleme-
    **                           ntary requests which have to share some state
    **                           for the lifespan of the session.
    ** 007 NVS 20050429   @20941 Fixed bugs in access control code for open,
    **                           closeBatch and isEditing methods.
    ** 008 NVS 20050504   @21066 Implemented finalization of security contexts
    **                           Contexts are assigned to sessions upon crea-
    **                           tion. terminateSession() unassigns context
    **                           from session and deletes session. assign()
    **                           and unassign() calls are balanced. The unas-
    **                           sign() call which zeros the use counter is
    **                           responsible for calling cleanup() method for
    **                           the security context.
    ** 009 NVS 20050505   @21107 Some major rework of the initial security
    **                           context handling and the way the generations
    **                           of the security caches coexist to fix the bug
    **                           documented in ##21100-21105.
    ** 010 NVS 20050524   @21273 This class now implements the Scope interface
    **                           which is required by the new Expression Engi-
    **                           ne. The scopes are nested so that plugins
    **                           have the Security Manager's scope as their
    **                           parent. Also, instead of separate compute()
    **                           and evaluate() methods a generic evaluate()
    **                           method is implemented which returns Object.
    ** 011 NVS 20050725   @21816 Implemented the getUserId() method required
    **                           to support the Progress USERID builtin func-
    **                           tion.
    ** 012 NVS 20050726   @21828 Added methods to retrieve and set arbitrary
    **                           account extension data of types int, boolean,
    **                           String and byte[].
    **                           method is implemented which returns Object.
    ** 013 SIY 20050726   @21830 Changed DirectoryService method name for
    **                           restricted use method call closeBatch from
    **                           closeBatch to closeBatchInt.
    ** 014 NVS 20050727   @21868 Initial security context of security cache is
    **                           now permanently bound to DirectoryService
    **                           upon creation. The call to securityIsUp
    **                           method relocated from SecurityCache constru-
    **                           ctor to SecurityManager constructor as this
    **                           is one time thing.
    ** 015 NVS 20050812   @22089 Bug fix: anonymous processes could not login.
    **                           The problem was in authenticateLocal() method
    **                           trying to pass the anonymous process ID as a
    **                           user ID to createSecurityContext() call.
    ** 016 NVS 20050831   @22446 Directory service is instantiated outside of
    **                           and before the security manager. This allows
    **                           directory based initialization of the server
    **                           application.
    ** 017 NVS 20050912   @22692 Implemented authentication errors recovery.
    **                           For those cases where the user input is
    **                           required (ID + PW, Certificate + ID + PW,
    **                           Custom), authentication protocol allows for a
    **                           configurable number of retries (unlimited
    **                           retries and no retries are also possible).
    ** 018 NVS 20050914   @22710 Added clientFinalize() method to comply with
    **                           the changed Authenticator interface.
    ** 019 NVS 20051003   @22926 Fixed a small bug in custom authentication
    **                           hook invocation failure handling.
    ** 020 ECF 20060120   @24006 Added variants of {add|remove|has|get}Token
    **                           which are package private and use Object keys
    **                           instead of Strings. The public, String-key
    **                           variants of these methods are now deprecated.
    **                           They must be removed once client code
    **                           dependencies have been resolved.
    ** 021 GES 20060122   @24012 Added getAccountIds() method to provide a
    **                           list of all valid subject IDs for the current
    **                           context.
    ** 022 ECF 20060123   @24036 Added session and thread ID constructs. A
    **                           unique session ID is assigned to each context
    **                           as it is created. A unique thread ID is
    **                           mapped to a thread at the time it is assigned
    **                           a context (an unmapped when the context is
    **                           unassigned). Both are accessed via read-only
    **                           getter methods.
    ** 023 NVS 20060217   @24614 Server's certificate validation is performed
    **                           only if explicitly or implicitly requested.
    **                           It is requested implicitly if client runs
    **                           with either keystore or truststore or both.
    **                           If neither is specified, the validation can
    **                           be requested explicitly using "security",
    **                           "certificate", "validate" config item set to
    **                           "true".
    ** 024 GES 20060305   @24897 Reworked logging to use J2SE instead of
    **                           stdio.
    ** 025 GES 20060313   @25014 Fixed logging init to set the default level.
    ** 026 NVS 20060313   @25155 Fixed synchronization bug in an inner class.
    **                           Removed some debugging output.
    ** 027 NVS 20060328   @25243 Added two new getExtDate(...) methods.
    ** 028 NVS 20060330   @25285 Added new methods: isUserDefined() and
    **                           getAllUsers().
    ** 029 NVS 20060331   @25406 Added new methods: getIdByExtInteger() and
    **                           getIdByExtString().
    ** 030 NVS 20060412   @25503 Added new methods:getAllSubjects() and
    **                           getIdByOrdinal(). Protected all recently
    **                           introduced methods with the system/accounts
    **                           and system/extensions resource access rights
    **                           check.
    ** 031 NVS 20060414   @25575 Added new method setPassword() that supports
    **                           dynamic password change. Added new method
    **                           changePassword() that initiates the password
    **                           change procedure from application.
    ** 032 NVS 20060418   @25584 Password input hook instantiation had to be
    **                           delayed till the use time.
    ** 033 NVS 20060426   @25729 Fixed thread ID assignment for the purpose of
    **                           describing security contexts.
    ** 034 NVS 20060426   @25744 Fixed password change implementation to try
    **                           adding password if there is no one yet. Fixed
    **                           password age calculation.
    ** 035 ECF 20060529   @26596 Replaced WeakHashMap with HashMap for thread
    **                           IDs. Changed default logging output to STAT.
    ** 036 ECF 20060614   @27245 Optimized auditing. Only compose audit
    **                           entries if auditing is enabled.
    ** 037 NVS 20060426   @27378 authenticateClient now is called from gearUp
    **                           method of ClientBootstrap.
    ** 038 GES 20060728   @28212 Rework of the checkCaller() interfaces and
    **                           backing methods to use common code and to
    **                           be *significantly* faster. The largest
    **                           performance enhancement comes from the
    **                           array form where previous usage got a new
    **                           copy of the same call stack for every entry
    **                           in the array that needed to be checked.
    ** 039 GES 20061002   @30110 Reworked client authentication processing to
    **                           pass a return code which specifies a more
    **                           precise reason for any failure.  This allows
    **                           the client auth hook to implement different
    **                           logic. As part of this change, much of the
    **                           code was reworked, refactored and cleaned
    **                           up.  In addition, the requirement that there
    **                           be a server-side custom auth plugin is eased
    **                           (depending on the way it is registered) in
    **                           the case that the registration is done of
    **                           a base name that then must have the "Client"
    **                           added to it.  If one explicitly registers a
    **                           full class name, then this will be used on
    **                           the server too.
    ** 040 GES 20061010   @30314 Honor a configuration value for debug level
    **                           which optionally may be present in the
    **                           directory.  If present, this overrides the
    **                           default debug level which is hard coded in
    **                           this class.
    ** 041 GES 20061215   @31708 Changes in bootstrap config interface.
    ** 042 GES 20070111   @31800 Match changes in the Authenticator interface.
    **                           Handled some cleanup and refactoring
    **                           including the elimination of AuthUIHelper and
    **                           the simplification of external interfaces.
    **                           Matched caller name changes and exposed
    **                           stack checking methods externally.
    ** 043 GES 20070115   @31839 Changed authorized callers for context mgmt.
    ** 044 GES 20070118   @31878 Added some TODOs, documentation and fixed
    **                           a potential null pointer exception.
    ** 045 NVS 20070316   @32414 Added closeBatch() method with controlled
    **                           security cache refresh.
    ** 046 NVS 20070411   @32933 Security threads are created as part of a
    **                           thread group named "security". These are
    **                           security cache refresh thread ("refresh") and
    **                           password change thread ("passwordchange").
    **                           Transient thread IDs are now deleted from the
    **                           thread ID map when they drop the initial
    **                           security context.
    ** 047 NVS 20070412   @32967 Reworked security context cleanup. cleanup()
    **                           method of SecurityContext, which is made
    **                           package private, is called from
    **                           popContextWorker() method when the context is
    **                           going out of use. The context remains
    **                           accessible for the current thread for the
    **                           lifespan of the cleanup() method call.
    ** 048 GES 20071011   @35428 Added safety code and cleaned up misc code
    **                           formatting/docs.
    ** 049 ECF 20071112   @35902 Changed many methods to accommodate net
    **                           package refactoring. Most restricted use
    **                           locations changed as a result. Added method
    **                           getEffectiveContext() (restricted) to query
    **                           effective context from net package. Removed
    **                           methods deprecated in #020 (@24006), as all
    **                           dependencies on these methods have now been
    **                           removed from use.
    ** 050 ECF 20071127   @36046 Fixed a minor defect in authenticateLocal().
    **                           Replaced & operator where && was required.
    ** 051 NVS 20080513   @38274 Fixed context cleanup when it happens in
    **                           the reader thread through terminateSession()
    **                           method. Fixed logging for going context.
    ** 052 ECF 20081006   @40039 Added variant of debug(). Accepts an
    **                           Exception in order to output a stack trace.
    ** 053 GES 20081024   @40341 Added support for sessions to be established
    **                           on insecure sockets (non-SSL).
    ** 054 NVS 20090130   @41245 Added support for programmatic authentication
    **                           type which is intended for use with admin
    **                           applet etc. The idea is that the applet needs
    **                           ID+PW authentication no matter what type is
    **                           set on the server for interactive users (this
    **                           may be a custom authentication plugin). This
    **                           authentication type is less secure per se,
    **                           AND, given the fact that it will be used to
    **                           administer the system, the implementation has
    **                           to be fortified somehow A.S.A.P. (since the
    **                           applet that uses this authentication gets
    **                           downloaded from the web server part of the
    **                           P2J server, the applet can get a token with a
    **                           limited lifespan that is also valid only if
    **                           returned from the same IP address).
    ** 055 JJC 20090208   @41406 Added FILE_SEPARATOR = "\\" + File.separator,
    **                           which works on Windows and Linux.
    **                           Changed logging so that the console handler
    **                           is turned off if not on the server. This
    **                           avoids permissions issue in the admin applet.
    ** 056 NVS 20090312   @41523 Added adminGetServerName() method.
    ** 057 NVS 20090318   @41629 Fixed a latent bug in SecurityCacheRefresh
    **                           inner class. Added adminGetCacheSerial() and
    **                           adminRefresh() methods.
    ** 058 NVS 20090323   @41668 Client side logic now allows the bootstrap
    **                           configuration of the same type to replace the
    **                           cached one.
    ** 059 NVS 20090326   @41683 Some changes due to creation of SecurityAdmin
    **                           class.
    ** 060 NVS 20090415   @41801 Cache generation messages can be routed to
    **                           admin client, server console or both ways.
    ** 061 NVS 20090417   @41809 Fixed a NPE in getIdByExtInteger() method and
    **                           prevented a similar NPE in getIdByExtString().
    ** 062 SIY 20090515   @42189 Added one more legal caller for
    **                           setInitialSecurityContext().
    ** 063 NVS 20090521   @42421 Now using an external implementation of the
    **                           password hashing algorithm.
    ** 064 NVS 20090528   @42481 Custom account extension plugins are read
    **                           from the directory and made available through
    **                           getCustomServerExt(), getCustomClientExt().
    ** 065 NVS 20090602   @42549 Custom account extension plugin jar file name
    **                           is read from the directory and made available
    **                           through getCustomClientJar() method.
    ** 066 NVS 20090603   @42597 Supporting account enablement status.
    ** 067 NVS 20090604   @42598 Supporting explicit password protected status.
    ** 068 NVS 20090615   @42697 Removed #065 due to a utilization of a simpler
    **                           approach.
    ** 069 GES 20090617   @42762 Added getSessionReport().
    ** 070 GES 20090619   @42784 Added killSession(). Fixed inherent problems with
    **                           authenticateClientWorker() which temporarily
    **                           switched bootstrap config instances for the
    **                           singleton security manager instance WITHOUT any
    **                           protection and WITHOUT any assurance that other
    **                           SM code wouldn't access the (invalid client cfg)
    **                           during the same period of time. This would lead to
    **                           unexpected failures that couldn't be easily
    **                           reproduced. This stems from the dual usage of
    **                           that worker for server-to-server connections. Also
    **                           in that case, no session object is kept in the
    **                           list of sessions since normal termination rules
    **                           do not apply.
    ** 071 NVS 20090624   @42843 Initial startup may need to write to the directory
    **                           for converting ACLs. Made it possible through
    **                           checks of the initialized status.
    ** 072 ECF 20090813   @43614 Added isGroupDefined(). Refactored worker code
    **                           from isUserDefined() into getAccount() method.
    ** 073 GES 20090816   @43640 Allow terminateSession() from Queue.stop().
    ** 074 NVS 20090816   @43651 The custom auth plugin can tell the server it wants
    **                           to exit silently by returning an empty array of
    **                           authentication data.
    ** 075 CA  20090824   @43721 The "Error getting AUTHDATA" message in
    **                           authenticateLocal is no longer logged as SEVERE.
    ** 076 CA  20090902   @43822 More log-level changes in authenticateLocal -
    **                           SocketException's are caught and logged using
    **                           dTrace().
    ** 077 NVS 20090909   @43849 Fixed retry loop in authenticateClient(). The loop
    **                           is interrupted in AUTH_MODE_IDPW if the source of
    **                           the input is the bootstrap config, since the result
    **                           won't change.
    ** 078 NVS 20090918   @43954 Made hasToken() and getToken() methods public.
    ** 079 NVS 20090925   @44036 Password change is better synchronized with the
    **                           security cache refresh. The changed password is
    **                           always stored it the most recent secury cache
    **                           generation.
    ** 080 NVS 20091007   @44109 adminAccess() queries the AdminResource plugin and
    **                           falls back to the SystemResource if not installed.
    ** 081 GES 20091103   @44306 Added logging to client authentication and allowed
    **                           changing the logging level on a client system.
    ** 082 CA  20091026   @44197 Added methods to build a consolidated set of ACLs.
    **                           This implies adding new permission search methods,
    **                           to determine the rights for a subject other than
    **                           the authenticated one.
    ** 083 CA  20091119   @44432 Added SessionToken implementation - an unique
    **                           object is kept for each security context, which
    **                           will hold the session ID and the subject.
    ** 084 GES 20100519   @44859 Virtual sessions now have a string as their session
    **                           ID object instead of a NetSocket. This breaks
    **                           session kill and session listing for the admin
    **                           interface.  This code is now protected.
    ** 085 SVL 20100604   @44901 Added isProcessDefined function.
    ** 086 SVL 20100706          Fixed authentication for users using only a
    **                           certificate: it does not fall to custom
    **                           authentication anymore.
    ** 087 SVL 20100709          Do not allow a user to login using user ID +
    **                           password if he has "certificate only" auth type.
    ** 088 SVL 20100713          Do don't override default auth mode by user's
    **                           auth mode AUTH_MODE_NONE.
    ** 089 SVL 20100916          User auth mode and auth plugin are preserved.
    **                           Client can send target user id at the beginning
    **                           of the authentication process.
    ** 090 SVL 20100923          Changed to conform the new Authenticator API.
    ** 091 CA  20101214          Solved memory leak on client side, when multiple
    **                           sessions are being created during the same run
    **                           (no security.Session needs to be created on
    **                           client side, as there is no security context).
    ** 092 CA  20110105          Fixed concurrency issues related to ACL searches.
    **                           After authentication, send the authentication
    **                           details (process and/or user ID) back to the
    **                           requester side.
    ** 093 CA  20110114          Fixed a NPE in rights searching (caused by H092).
    ** 094 CA  20110131          Changes to allow the direct sessions to authenti-
    **                           cate using different credentials. Also, this way
    **                           sessions can be created to different router nodes.
    ** 095 LMR 20101215          Changed authenticateClientWorker() to default
    **                           net/connection/secure to false.
    ** 096 GES 20111003          Changed obtainCustomClientHook() to try to load
    **                           a hook class from the classpath if the byte[]
    **                           cannot be loaded (this happens when the client is
    **                           running in an applet).
    ** 097 GES 20111028          Added authenticateSingle() and terminateSingle()
    **                           which provide a mechanism to login and create an
    **                           interactive client session where the client code
    **                           runs in the server process. Normally, the security
    **                           authentication and session termination is fully
    **                           integrated and driven by the setup of a network
    **                           session. In the local mode, there is no network
    **                           session, but the security context management must
    **                           still be done.
    ** 098 GES 20111110          Modifications to provide the ability to have
    **                           unique initial contexts (that are separate from
    **                           the standard server's initial context). These are
    **                           used when the client is run in the server JVM and
    **                           it is important to enable multiple clients in the
    **                           same JVM (since they must not co-mingle their
    **                           contexts or context-local data). It is also used
    **                           to make sure no client state is mingled with
    **                           the standard server thread's initial context
    **                           (which is shared for all server threads).
    ** 099 CA  20111209          Added method to list active sessions which have
    **                           invoked APIs in the given jar or interface.
    **                           checkCallerAbort now allows to check functions
    **                           of SecurityManager.
    ** 100 CA  20120727          The context cleanup code must be executed by only
    **                           one thread.  If other threads reach the cleanup
    **                           code for the same context, it will no-op when
    **                           another thread has started the cleanup for the
    **                           same context.
    ** 101 ECF 20130321          Enable authenticateRemote to work if only a process ID is provided.
    ** 102 CA  20130529          Changes for the appserver implementation.
    ** 103 CA  20130628          Added possibility to reset a context (needed by appservers running in
    **                           State-reset operating mode).
    ** 104 CA  20130704          Added API to get the accounts for a specified appserver.
    ** 105 CA  20131122          Protected security "cache" field access against possible concurrency
    **                           problems.  During authentication: if a newer security cache
    **                           generation is referenced, do not allow authentication; always use
    **                           the security cache generation saved at the begining of the auth
    **                           process, throughout the authentication.
    ** 106 OM  20131002          Added support for multiple entity authenticator hooks.
    **                           Generified private members. Autoboxing. Removed useless String c'tors.
    ** 107 MAG 20131219          Added API to update user cached accounts.
    ** 108 CA  20140206          Appservers can be associated only with P2J process accounts.
    **                           Added helper APIs related to appserver launching.
    **                           Implemented in-directory private keys.
    ** 109 CA  20140225          Added one more authorized "setInitialSecurityContext" caller.
    ** 110 MAG 20140310          Added method to get the systemPassword node.
    ** 111 CA  20140714          Fixed HookClassLoader, to allow the p2j.jar to be deployed in a web
    **                           application container.
    ** 112 MAG 20140715          Added method to get broker name for a process account.
    ** 113 CA  20150131          Clean the security caches when client restarts.
    ** 114 ECF 20150726          Removed redundant Throwable.fillInStackTrace() call; replaced
    **                           StringBuffer with StringBuilder.
    ** 115 ECF 20150828          Enable Java 8.
    ** 116 OM  20160127          Implemented isServerAccount().
    ** 117 CA  20160205          Reworked getEncryptedKeyStore() to return a method reference, if the
    **                           thread has the server context.
    ** 118 EVL 20160224          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 119 IAS 20160331          Fixed debugLevel protection - use ReentrantReadWriteLock
    ** 120 EVL 20160405          Fix for reconstruction class filename inside jar.  The separator must
    **                           always be "/" not depending on the OS behind.
    ** 120 IAS 20160408          Use synthetic key as context key.
    ** 121 HC  20170612          Changes related to implementation of new GWT-based Admin client.
    ** 122 SBI 20171116          Changed getEncryptedKeyStoreWorker(..) and added getWebServerAlias().
    ** 123 CA  20180212          Fixed terminateSessionWorker - all SecurityContext.cleanup() calls
    **                           must be protected via the cleanupLocks.get(ctx) flag.
    ** 124 CA  20180217          Virtual sessions can't cleanup their context concurrently, as they
    **                           might perform remote calls (for e.g. database disconnection) - thus,
    **                           they need to use a common lock.
    ** 125 CA  20180313          Javadoc fix.
    ** 126 CA  20180509          Added getPeerHost() and getPeerPort().
    **     CA  20180514          Added getServerId - retrieves the server's account ID.
    ** 127 CA  20180607          Added authenticateServer(BoostrapConfig) - allows a no-context in-JVM
    **                           thread to authenticate via a certificate.
    ** 128 CA  20190718          Added a 'context switcher' helper class, for which an instance is
    **                           created only and only if the authentication worker has succeeded.
    **     CA  20190902          Fixed problems when connecting to a FWD server running a database
    **                           schema which is not actively managed by the requester, too.
    ** 129 GES 20200206          Some light refactoring and new features added to allow creation of
    **                           a new session when forceSecurityContext() is used.
    **     CA  20200213          Added setHeadless.
    ** 130 IAS 20200722          Refactored to the new NetSocket API.
    **     HC  20201024          Added exception info to an error message.
    ** 131 IAS 20201214          Additional initialization added to the constructor.
    **     ECF 20210304          Reduced default logging level to WARNING.
    *      IAS 20210608          Optionally use BouncyCastle JCR and JSSE providers.
    **     IAS 20210827          Added allowed ciphers' filtering.
    **     GES 20210920          Reworked to enforce user session limits.
    **     CA  20220405          Added authentication and authorization for web requests.  When this is enabled,
    **                           the target API call will be executed under the authenticated FWD context, and not
    **                           the agent's context.
    **     IAS 20220624          Added support for the Conscrypt JCE/JSSE provider
    **     TJD 20220504          Java 11 compatibility minor changes
    ** 132 HC  20230427          Implemented management for file-system resource. This includes Admin
    **                           and server directory changes.
    ** 133 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 134 GBB 20230523          Change the logger level to WARNING only if no configs have been set.
    ** 135 CA  20230531          Moved the scope state from the resolver to the actual scope; this avoids using
    **                           a WeakHashMap to determine the scope's state associated with a certain resolver,
    **                           as long as a single resolver instance uses that scope (common for conversion).
    ** 136 CA  20230731          Replaced 'threadIDs' map with a ThreadLocal field, which gets set only once.
    ** 137 GBB 20230825          Support for OS users and SSO authenticator. Closely related methods moved to
    **                           wrapper classes to solve ambiguity for some and improve the cohesion. Single
    **                           responsibility by moving Authenticator impl in its own class.
    **     CA  20231018          Fixed 'terminateSessionById' where the 'destroyWebRequestContext' caller was
    **                           moved to 'LegacyWebSecurityManager'.
    ** 138 CA  20231026          Added GenericWebServer session cleaner's call to 'terminateSessionById'
    ** 139 GBB 20231117          Added SessionSecurityManager.addSessionDetails. Fix for sso session id.
    ** 140 GBB 20231130          driverType renamed to driverName.
    ** 141 GBB 20240215          Method killSession() made public and restricting use.
    ** 142 GBB 20240301          setupClient renamed to setupSession in caller check for addSessionDetails().
    **                           Session renamed to SecuritySession to resolve ambigious imports.
    ** 143 TJD 20240123          Java 17 compatibility updates
    ** 144 ES  20240612          Added getCurrentContextControl and stopAfterInterrupt methods in order to secure
    **                           access to Control.
    ** 145 GBB 20240709          Hard-coded config names replaced by ConfigItem constants.
    ** 146 GBB 20240730          OS user methods to use security cache instead of directory.
    ** 147 CA  20240814          'terminateSessionWorker' must drop the initial context if it was set in this call.
    ** 148 GBB 20240826          Removing single client relevant methods.
    ** 149 GBB 20241010          Adding getCurrentSession.
    ** 150 RNC 20250109          Added a cache map in SessionSecurityManager.
    *      RNC 20250115          If the client receives AUTH_ACTION_LOCKED from the server while trying to be
    *                            authenticated, it means new sessions can't be launched, so close the socket.
    ** 151 GBB 20250403          Making createSecurityContext package-private with call checks.
    ** 152 AI  20250226          Adjusted getUserId() to use SSO storageId if available.
    */

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

    package com.goldencode.p2j.security;

    import java.io.*;
    import java.net.*;
    import java.security.*;
    import java.security.cert.*;
    import java.security.cert.Certificate;
    import java.util.*;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.atomic.*;
    import java.util.function.*;
    import java.util.logging.*;
    import java.util.stream.*;

    import javax.net.ssl.*;

    import com.goldencode.expr.*;
    import com.goldencode.p2j.admin.*;
    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.main.ServerKeyStore.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.directory.Base64;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.CentralLogger;
    import com.goldencode.p2j.util.Utils.DirScope;
    import com.goldencode.util.*;

    /**
     * This class is the core of P2J security management.
     * <p>
     * Details on client authentication protocol:
     * <br>
     * Once the TLS connection is established and certificates are exchanged and
     * verified:
     * <ul>
     *   <li>if the connection is anonymous (client didn't send certificate):
     *     <ul>
     *       <li>the client sends the AUTHTYPE packet, where the content can be
     *           either AUTH_REQ_PROCESS or AUTH_REQ_USER; if AUTHTYPE is
     *           AUTH_REQ_USER then id of the target user is send (it may be empty
     *           if it is not know at the beginning of authentication process).
     *       <li>for AUTH_REQ_PROCESS, the server immediately sets AUTH_MODE_REQ
     *           to either SUCCESS (if anonymous connections are allowed, or
     *           FAILURE otherwise;
     *       <li>the case of AUTH_REQ_USER processing continues with the next step
     *     </ul>
     *   <li>the server sends the AUTHMODEREQ packet;
     *   <li>the client reads the AUTHMODEREQ packet (with an authentication mode
     *       and a disposition/action code) and checks the disposition;
     *       if the disposition is either AUTH_ACTION_ABORT (on fatal error) or
     *       AUTH_ACTION_DONE (on success due to certificate based or anonymous
     *       authenticaion), the procedure is over; otherwise it continues with
     *       the next step;
     *   <li>if the mode is AUTH_MODE_CUSTOM, the server should have sent and
     *       the client reads AUTHCUSTOM packet which delivers the class name and
     *       parameters of the custom authentication hook;  this hook class is
     *       run (which can display any UI and interact with the user OR it may
     *       access any device/facility such as a smart card);  the result is a
     *       userid and password
     *   <li>if the mode is AUTH_MODE_IDPW or AUTH_MODE_X509_IDPW, then a generic
     *       client side processing is performed which reads the userid and
     *       password
     *   <li>either custom or standard modes produce a byte array of
     *       authentication data which are sent in AUTHDATA packet back to server;
     *   <li>server responds with AUTHRESULT packet (includes a return code and
     *       a disposition/action code) which reports either AUTH_ACTION_ABORT
     *       (on fatal failure), AUTH_ACTION_RETRY (retryable failure) or
     *       AUTH_ACTION_DONE (on success)
     *   <li>on the client, any action of AUTH_ACTION_RETRY will cause the
     *       processing to repeat starting with the AUTHMODEREQ packet; note that
     *       the return code sent in the AUTHRESULT packet will be provided to
     *       the authentication plugin at that time;
     *   <li>on the client, any action of AUTH_ACTION_ABORT causes the processing
     *       to halt and any action of AUTH_ACTION_DONE authentication has
     *       successfully completed, either way the processing is over;
     * </ul>
     * The AUTHDATA packet for the standard modes is made of a <code>String</code>
     * userID followed by <code>char[]</code> password. UserID is serialized using
     * <code>writeUTF()</code> method. Password is serialized using
     * <code>writeShort()</code> encoding the array length, plus
     * <code>writeChar</code> for every character in the array.
     * <p>
     * A unique session ID is assigned to each security context upon creation.
     * Whenever a thread is assigned to a context, it is mapped to a new ID.
     * While the thread IDs (which are integers) could theoretically wrap, such
     * that a thread associated with a very long running context could be assigned
     * an ID already in use, the combination of session ID and thread ID is
     * guaranteed always to be unique, and it is really only this combination
     * which is meaningful for practical use.
     */
    public final class SecurityManager
    extends Scope
    implements SecurityConstants
    {
       /** Wrapper for context related logic. */
       public final ContextSecurityManager contextSm = new ContextSecurityManager();

       /** Wrapper for session related logic. */
       public final SessionSecurityManager sessionSm = new SessionSecurityManager();

       /** Wrapper for certificate related logic. */
       public final CertificateSecurityManager certificateSm = new CertificateSecurityManager();

       /** Wrapper for virtual connection related logic. */
       public final VirtualConnectionSecurityManager virtualSm = new VirtualConnectionSecurityManager();

       /** Wrapper for SSO related logic. */
       public final SsoSecurityManager ssoSm = new SsoSecurityManager();

       /** Container for the legacy web security logic. */
       public final LegacyWebSecurityManager legacyWebSm = new LegacyWebSecurityManager(this);

       /** Container for the account ext logic. */
       public final AccountExtUtil accountExt = new AccountExtUtil(this);

       /** Authenticator instance. */
       final SecurityManagerAuthenticator serverAuthenticator = new SecurityManagerAuthenticator(this);

       /** Object for serializing password change requests. */
       final Object pwchSync = new Object();

       /** Logger. */
       private static final CentralLogger LOG = CentralLogger.get(SecurityManager.class);

       /** Constant used when no retries are allowed in authentication process. */
       private static final int ALWAYS_RETRY = -1;

       /** Reference to the singleton instance. */
       private static SecurityManager securityManager = null;

       /** Thread group for all security threads. */
       private static ThreadGroup secThreads = new ThreadGroup(
          Thread.currentThread().getThreadGroup(), "security");

       /** The bootstrap configuration. */
       private BootstrapConfig cfg = null;

       /** Caches context data. */
       private SecurityCache cache = null;

       /** Last used serial number for security cache creation. */
       private int cacheSerial = 0;

       /** Provides network socket security services. */
       private TransportSecurity tranSec = null;

       /** Next available, unique thread ID. */
       private int nextThreadID = 0;

       /** The thread ID, to uniquely identify the Java Thread. */
       private static final ThreadLocal<Integer> THREAD_ID =
          ThreadLocal.withInitial(() -> ++securityManager.nextThreadID);

       /** Flag to detect if we are initialized or not. */
       private boolean init = false;

       /** Records the current number of user accounts which have at least one active interactive session. */
       private int interactiveUsers = 0;

       /** Tracks the total number of interactive sessions currently active by user account name. */
       private Map<String, AtomicInteger> interactiveSessions = new HashMap<String, AtomicInteger>();

       /** Manages access to user limit state. */
       private Object userLimitLock = new Object();

       /** Map containing flags which inform other threads that cleanup for a
        * certain context is in progress, so they should skip it. */
       private Map<Object, Boolean> cleanupLocks = new HashMap<>();

       /** Use Galois/counter mode ciphers
        *  Disabling this mode will allow only "confidentiality only" modes
        *  (no AEAD) which can be more efficient
        */
       private final boolean useGCM;

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

       /**
        * Private constructor to enable this as a singleton class. Instantiation
        * occurs via {@link #createInstance}.
        *
        * @param  bc
        *         an instance of <code>BootstrapConfig</code> class used to get
        *         the security related configuration information
        *
        * @throws ConfigurationException
        *         may be thrown in SecurityCache
        * @throws NoSuchMethodException
        *         may be thrown in SecurityCache
        */
       private SecurityManager(BootstrapConfig bc)
       throws ConfigurationException,
              NoSuchMethodException
       {
          securityManager = this;

          // keep a copy of the bootstrap configuration
          cfg = bc;

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

          // instantiate the security cache
          SecurityCache newCache = new SecurityCache(bc, cacheSerial);
          Audit audit = newCache.getAudit();

          setCache(newCache);

          this.useGCM = bc.getBoolean("net", "ssl", "useGCM", true);

          if (!bc.isServer())
             return;

          ds.securityIsUp();

          try
          {
             audit.start();
          }
          catch (IOException exc)
          {
             // The newly defined audit log file can't be activated and
             // the old audit log's gone.
             LOG.severe("*** Audit log failure *** ", exc);
          }

          SecurityContext ctx = newCache.getInitialSecurityContext();
          contextSm.logNewContext(newCache, ctx);

          try
          {
             contextSm.setInitialSecurityContext();
          }
          catch (RestrictedUseException exc)
          {
             throw new ConfigurationException("setInitialSecurityContext()", exc);
          }

          try
          {
             contextSm.getSecureSocketContext();
          }
          catch (UnrecoverableKeyException | KeyManagementException | NoSuchAlgorithmException
                | KeyStoreException | ConfigurationException | NoSuchProviderException e)
          {
             throw new ConfigurationException("getSecureSocketContext()", e);
          }

          init = true;
       }

       /**
        * Clean the storage caches.
        *
        * @throws   RestrictedUseException
        *           If called outside of expected code.
        */
       public static synchronized void clearInstance()
       throws RestrictedUseException
       {
          // restrict the use of this method
          String[] callers = new String[]
          {
             "com.goldencode.p2j.main.ClientCore.start"
          };
          SecurityUtil.checkCallerAbort(callers);

          securityManager = null;
          ContextLocal.clearInstance();
       }

       /**
        * Instantiates SecurityManager as a singleton.
        *
        * @param  bc
        *         an instance of <code>BootstrapConfig</code> class used to get
        *         the security related configuration information
        *
        * @return instance of <code>SecurityManager</code>
        *
        * @throws ConfigurationException
        *         may be thrown in SecurityManager constructor
        * @throws NoSuchMethodException
        *         may be thrown in SecurityCache
        */
       public static synchronized SecurityManager createInstance(BootstrapConfig bc)
       throws ConfigurationException,
              NoSuchMethodException
       {
          if (securityManager == null)
          {
             securityManager = new SecurityManager(bc);
          }
          else
          {
             // if on the client and with the same client type config
             if (!securityManager.cfg.isServer() && !bc.isServer())
             {
                // replace the config with the new one
                securityManager.cfg = bc;

                if (bc.getBoolean("security", "transport", "refresh", false))
                {
                   // there are cases when the transport security needs to be invalidated and
                   // refreshed.  the client config will inform us of such cases.
                   securityManager.tranSec = null;
                }
             }
          }

          return securityManager;
       }

       /**
        * Returns the reference to the instance of SecurityManager.
        *
        * @return instance of <code>SecurityManager</code>
        */
       public static synchronized SecurityManager getInstance()
       {
          return securityManager;
       }

       /**
        * Returns a flag indicating the readiness of the security manager to
        * properly operate.
        *
        * @return   <code>true</code> if the security manager is ready to accept
        *           requests.
        */
       public static synchronized boolean initialized()
       {
          return (securityManager != null && securityManager.init);
       }

       /**
        * Find and update user account stored in cache.
        *
        * @param   user
        *          User account parameters.
        *
        * @return  <code>true</code> if account is found and updated.
        *          <code>false</code> if account not found.
        *
        * @throws  RestrictedUseException
        *          If called improperly.
        */
       public static synchronized boolean updateCachedUserAccount(UserDef user)
       throws RestrictedUseException
       {
          // restrict the use of this method
          SecurityUtil.checkCallerAbort("com.goldencode.p2j.main.UpdateAccountTask.run");
          // find and update account
          SecurityManager sm = SecurityManager.getInstance();
          SecurityCache cache =  sm.getCache();
          return cache.updateCachedUserAccount(user);
       }

       /**
        * Gives the context local information of a currently running thread.
        *
        * @return  Context local information.
        *
        * @throws RestrictedUseException
        *         If called improperly.
        */
       public static synchronized Control getCurrentContextControl()
       throws RestrictedUseException
       {
          // restrict the use of this method
          SecurityUtil.checkCallerAbort("com.goldencode.p2j.util.BlockManager.stopAfter");

          return Control.getCurrentContext();
       }

       /**
        * Sets the context local to a control of a Conversation thread.
        *
        * @param  control
        *         Control related to a Conversation thread.
        *
        * @throws RestrictedUseException
        *         If called improperly.
        */
       public static void stopAfterInterrupt(Control control)
       throws RestrictedUseException
       {
          // restrict the use of this method
          SecurityUtil.checkCallerAbort("com.goldencode.p2j.util.StopAfterTimer$Task.run");

          Control.stopAfterInterrupt(control);
       }

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

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

       /**
        * Compares the list of DNS names with the given host name. Leftmost * in
        * DNS names like *.tld matches any.name.tld. Comparison is case
        * insensitive.
        *
        * @param  host
        *         name to check
        * @param  list
        *         list of names
        * @return index of matching name in the list or -1 of no matches found
        */
       private static int hostMatch(String host, List<String> list)
       {
          if (host == null) // SSLSession.getPeerHost() can return null
          {
             return list.isEmpty() ? -1 : 0;
          }
          String name = null;
          String tld  = null;
          int i = 0;

          for (i = 0; i < list.size(); i++)
          {
             name = list.get(i);

             if (name.startsWith("*."))
             {
                // match *.tld case
                tld = name.substring(1);
                if (host.toUpperCase().endsWith(tld.toUpperCase()))
                   return i;
             }
             else
             {
                // match regular case
                if (host.compareToIgnoreCase(name) == 0)
                   return i;
             }
          }

          return -1;
       }

       /**
        * Get an instance of the in-process authentication class.
        *
        * @return   The instance of the custom client authentication hook.
        */
       private Authenticator obtainCustomAuthHook(String hookName)
       {
          Authenticator auth = null;

          try
          {
             String path = SecurityUtil.buildClassFileName(hookName);

             if (ClassLoader.getSystemResource(path) != null)
             {
                Class<?> hook = Class.forName(hookName);

                if (hook != null)
                {
                   auth = (Authenticator) hook.getDeclaredConstructor().newInstance();
                }
             }
          }

          catch (Exception e)
          {
             // something is wrong with the class
             LOG.severe("Error obtaining the authentication class. ", e);
          }

          return auth;
       }

       /**
        * Gets custom authentication class and parameters from the server and runs
        * the specified method.
        * <p>
        * The AUTHCUSTOM packet is two consecutive strings written using the
        * <code>writeUTF()</code> method. The first is the class name, followed
        * by the parameters. The no parameter case is handled as if it was an
        * empty string.
        *
        * @param    dis
        *           The input stream to read from.
        *
        * @return   The instance of the custom client authentication hook.
        */
       private Authenticator obtainCustomClientHook(NetSocket dis)
       {
          byte classData[] = null;
          int  classSize;
          String hookName;

          try
          {
             // the packet is made of three pieces:
             // 1 - byte array, copy of the class to run
             //     formatted as int + bytes, where int == 0 means no class
             // 2 - fully qualified class name formatted as UTF
             // 3 - string of class parameters formatted as UTF (not read here)

             // read piece 1 - copy of the class
             classSize = dis.readInt();
             if (classSize > 0)
             {
                classData = new byte[classSize];
                dis.readBytes(classData);
             }

             // read piece 2 - class name
             hookName = dis.readUTF();
          }
          catch (IOException io)
          {
             // something's wrong with the socket anyway
             LOG.severe("Error downloading the authentication class. ", io);
             return null;
          }

          // create an instance of the class and call clientAuthHook
          try
          {
             Class<?> client = null;

             if (classData != null)
             {
                try
                {
                   HookClassLoader hookLoader = new HookClassLoader(this.getClass().getClassLoader(),
                                                                    classData);

                   // this will load the class from its classpath version (if the hook is already
                   // known to the client). it will resolve it from the given bytecode only if the
                   // client doesn't know the class.
                   client = hookLoader.loadClass(hookName);
                }
                catch (Exception exc)
                {
                   LOG.severe("HookClassLoader failed. ", exc);

                   // fall back plan (check the classpath for a class of the same
                   // name (this path is used for applets)
                   client = Class.forName(hookName);
                }
             }
             else
             {
                client = Class.forName(hookName);
             }

             Authenticator instance = (Authenticator) client.getDeclaredConstructor().newInstance();
             return instance;
          }
          catch (Exception e)
          {
             // something's wrong with the class
             LOG.severe("Error loading the authentication class. ", e);
             return null;
          }
       }

       /**
        * Gets the server's account name.
        * This is an admin API support method.
        *
        * @param  scope
        *         <code>true</code> means using the latest security cache,
        *         otherwise the session security cache
        *
        * @return server's name or <code>null</code>
        */
       public String adminGetServerName(boolean scope)
       {
          if (!adminAccess())
          {
             return null;
          }

          SecurityCache sc = null;

          if (scope)
          {
             sc = getCache();
          }
          else
          {
             // Pick up the generation of security cache
             SecurityContextStack ctxs = SecurityContextStack.getContext();
             SecurityContext ctx = ctxs.getEffectiveContext();
             sc = ctx.getCache();
          }

          return sc.getServerAccount().getSubjectId();
       }

       /**
        * Gets the server's account name.
        * This is an admin API support method.
        *
        * @param  scope
        *         <code>true</code> means using the latest security cache,
        *         otherwise the session security cache
        *
        * @return security cache generation serial number
        */
       public int adminGetCacheSerial(boolean scope)
       {
          if (!adminAccess())
          {
             return -1;
          }

          SecurityCache sc = null;

          if (scope)
          {
             sc = getCache();
          }
          else
          {
             // Pick up the generation of security cache
             SecurityContextStack ctxs = SecurityContextStack.getContext();
             SecurityContext ctx = ctxs.getEffectiveContext();
             sc = ctx.getCache();
          }

          return sc.getSerial();
       }

       /**
        * Refreshes the security cache from the current state of the directory.
        * This is an admin API support method.
        *
        * @param    msgBuf
        *           list to receive messages if logmode enables admin messages
        *
        * @return   The security cache generation serial number.
        */
       public int adminRefresh(List<String[]> msgBuf)
       {
          // TO-DO granular check
          if (!adminAccess())
          {
             return -1;
          }

          // refresh the security cache
          SecurityCacheRefresh ref = new SecurityCacheRefresh(SecurityCache.LM_DUAL, msgBuf);

          Thread thread = new Thread(secThreads, ref, "refresh");
          thread.start();

          // wait till refresh is done
          try
          {
             thread.join();
          }
          catch (InterruptedException ie)
          {
             return -1;
          }

          return cacheSerial;
       }

       /**
        * Gets the parent scope for this scope.
        * Security Manager's scope has no parent.
        *
        * @return parent <code>Scope</code> object, which is <code>null</code>.
        */
       public final Scope getEnclosingScope()
       {
          return null;
       }

       /**
        * Set the context cleanup {@link #cleanupLocks lock}, to avoid concurrent cleanups.
        *
        * @param    context
        *           The {@link SecurityContext} instance to configure.
        * @param    lock
        *           The instance on which to lock for context cleanup.
        */
       public void setCleanupLock(Object context, Object lock)
       {
          if (!(context instanceof SecurityContext))
          {
             return;
          }

          ((SecurityContext) context).setCleanupLock(lock);
       }

       /**
        * Performs the requester side of a process or interactive client
        * authentication. Based on configuration, this method can validate the
        * server's certificate.
        * <p>
        * If required, the server's X.509 certificate is validated first. If this
        * does not fail, its DNS name is checked as defined in RFC2830, p3.6:
        * <ul>
        *   <li>if the certificate contains subjectAlternateNames of type dNSName,
        *       the set of those names is taken;
        *   <li>otherwise, the CN (common name) portion of the subject's DN is
        *       taken
        *   <li>the resulting set of names is compared against the hostname used in
        *       establishing the TLS connection
        *   <li>'*' can be used as the leftmost component of the DNS name; it
        *       matches any name in that position
        * </ul>
        * If the DNS name check fails, an alternative check is done: the server's
        * certificate is compared with the certificate pointed to by the
        * truststore alias. The comparison is done for encoded certificates and
        * they have to be identical.
        * <p>
        * The next and last step is the client authentication procedure.
        * <p>
        * <i><u>Restricted use</u></i>. This method checks the caller to be
        * <code>com.goldencode.p2j.net.SessionManager.createQueue()</code>.
        *
        * @param   socket
        *          The socket identifying this connection.
        * @param   config
        *          BootstrapConfig instance which should override the security
        *          manager's version;  <code>null</code> to use default config.
        *
        * @return  The data used to authenticate, or <code>null</code> if authentication failed.
        *
        * @throws  RestrictedUseException
        *          If called improperly.
        */
       public AuthData authenticateClient(NetSocket socket, BootstrapConfig config)
       throws RestrictedUseException
       {
          // restrict the use of this method
          SecurityUtil.checkCallerAbort("com.goldencode.p2j.net.SessionManager.createQueue");

          if (config == null)
          {
             config = this.cfg;
          }

          if (config.isServer())
          {
             return null;
          }

          synchronized (SecurityManager.class)
          {
             return authenticateClientWorker(socket, config);
          }
       }

       /**
        * Core worker for {@code authenticateClient()}.  Temporarily uses the provided configuration instance.
        *
        * @param   socket
        *          The socket identifying this connection.
        * @param   config
        *          BootstrapConfig instance which should override the security
        *          Can't be <code>null</code>.
        *
        * @return  The data used to authenticate, or <code>null</code> if authentication failed.
        */
       private AuthData authenticateClientWorker(NetSocket socket, BootstrapConfig config)
       {
          boolean isProcess = false;
          String remoteAuthID = null;
          String appServer = "";

          boolean secure = config.getBoolean("net", "connection", "secure", false);

          boolean isFinerLoggable = LOG.isLoggable(Level.FINER);
          if (isFinerLoggable)
          {
             LOG.finer("secure = " + secure);
          }

          // certificate processing

          SSLSession locSess = socket.getSession();

          if (locSess == null && secure)
          {
             if (isFinerLoggable)
             {
                LOG.finer("Secure mode requires a certificate sent, which is missing.");
             }
             socket.close();
             return null;
          }

          // query local certificate
          java.security.cert.Certificate[] locCerts = null;

          if (secure)
          {
             locCerts = locSess.getLocalCertificates();
          }

          boolean sentCert = (locCerts != null && locCerts.length > 0);

          if (isFinerLoggable)
          {
             LOG.finer("sentCert = " + secure);
          }

          // validate the server's certificate if requested
          if (secure && !certificateSm.validateServer(locSess, config))
          {
             if (isFinerLoggable)
             {
                LOG.finer("Server certificate validation failed.");
             }
             socket.close();
             return null;
          }

          if (!sentCert)
          {
             String clientUuid = config.getString(ConfigItem.SPAWNER_UUID, null);
             String userName = config.getString(ConfigItem.SSO_SUBJECT_ID, null);

             if (clientUuid != null && userName != null)
             {
                return ssoSm.authenticateClientWorkerSso(socket, clientUuid, userName);
             }

             // state the client's intention in case of anonymous connection
             if (!sendAuthType(socket, config))
             {
                if (isFinerLoggable)
                {
                   LOG.finer("Anonymous connection refused.");
                }
                socket.close();
                return null;
             }
          }

          // retryable section of the authentication protocol
          int authModeReq = AUTH_MODE_NONE;
          int authResult  = AUTH_RESULT_NONE;
          int authAction  = AUTH_ACTION_CONTINUE;
          Authenticator auth   = null;
          String        entity = null;

          try
          {
             retryLoop:
             while (authAction == AUTH_ACTION_CONTINUE || authAction == AUTH_ACTION_RETRY)
             {
                if (isFinerLoggable)
                {
                   LOG.finer("Getting AUTHMODEREQ");
                }

                // read the AUTHMODEREQ packet from the server
                try
                {
                   authModeReq = socket.readInt();
                   authAction  = socket.readInt();
                }
                catch (IOException ioe)
                {
                   if (isFinerLoggable)
                   {
                      LOG.finer("Error getting AUTHMODEREQ " + ioe);
                   }
                   authAction = AUTH_ACTION_ABORT;
                }

                if (isFinerLoggable)
                {
                   LOG.finer("Got AUTHMODEREQ: " + authModeReq + " (action = " + authAction + ")");
                }

                // interpret AUTHMODEREQ packet contents
                switch (authAction)
                {
                   case AUTH_ACTION_ABORT:
                      if (isFinerLoggable)
                      {
                         LOG.finer("Aborting as requested.");
                      }
                      socket.close();
                      return null;

                   case AUTH_ACTION_DONE:
                      if (isFinerLoggable)
                      {
                         LOG.finer("Done with retry loop.");
                      }
                      break retryLoop;
                   case AUTH_ACTION_LOCKED:
                      if (isFinerLoggable)
                      {
                         LOG.finer("New sessions are disabled on the FWD server.");
                      }
                      socket.close();
                      return null;
                }

                byte[] authData = null;

                // some auth mode types are not done yet, execute next phase
                switch (authModeReq)
                {
                   case AUTH_MODE_X509:
                      // This mode means the client's certificate was required
                      // to be sent previously (at TLS connection time) but it
                      // was not sent.  This is a protocol logic issue since we
                      // should have aborted above.
                      if (isFinerLoggable)
                      {
                         LOG.finer("Missing certificate (protocol error).");
                      }
                      socket.close();
                      return null;

                   case AUTH_MODE_IDPW:
                      // This mode means that the server wants ID and password and
                      // does not care about client's certificate whether it was
                      // sent or not, fall-through.

                   case AUTH_MODE_X509_IDPW:
                      // This mode means that the server wants ID and password as
                      // well as the client's certificate which was sent (otherwise
                      // AUTH_RESULT_FAILURE would be received).
                      authData = serverAuthenticator.clientAuthHookWorker(null, authResult, config);
                      if (authData == null)
                      {
                         if (isFinerLoggable)
                         {
                            LOG.finer("clientAuthHookWorker failed.");
                         }
                         socket.close();
                         return null;
                      }

                      auth = serverAuthenticator;
                      break;

                   case AUTH_MODE_CUSTOM:
                      // This mode means that the server runs custom
                      // authentication hook regardless of the certificate. The
                      // call below may return null.  This typically loads a
                      // class via data sent on the connection and returns an
                      // instance of that class.  Note this only happens the
                      // first time we come through).
                      if (authResult == AUTH_RESULT_NONE)
                      {
                         // reads the first 2 pieces of the packet, loads the
                         // class and returns an instance
                         auth = obtainCustomClientHook(socket);
                         try
                         {
                            // the packet is made of three pieces:
                            // read piece 3 - class parameters
                            entity = socket.readUTF();
                         }
                         catch (IOException io)
                         {
                            if (isFinerLoggable)
                            {
                               LOG.log(Level.FINER, "Problem reading hook parm ", io);
                            }
                            entity = "";
                         }
                      }

                      if (auth != null)
                      {
                         Map<String, Object> parameters = new HashMap<>();
                         parameters.put("option", entity);
                         try
                         {
                            parameters.put("subjectId", cfg.getConfigItem("access", "subject", "id"));
                         }
                         catch (ConfigurationException e)
                         {
                            // ignore
                         }

                         authData = auth.clientAuthHook(parameters, authResult);
                      }
                      else
                      {
                         if (isFinerLoggable)
                         {
                            LOG.finer("Custom client auth hook failed.");
                         }
                         socket.close();
                         return null;
                      }
                      break;
                }

                // provide authentication data to the server
                int authdl = authData == null ? PKT_SIZE_SKIP_TO_NEXT : authData.length;

                if (isFinerLoggable)
                {
                   LOG.finer("Sending AUTHDATA");
                }
                try
                {
                   // send the packet
                   socket.writeInt(authdl);
                   if (authdl > 0)
                   {
                      socket.writeBytes(authData);
                   }
                   socket.flush();
                }
                catch (IOException ioe)
                {
                   if (isFinerLoggable)
                   {
                      LOG.log(Level.FINER, "Failure sending AUTHDATA ", ioe);
                   }
                   socket.close();
                   return null;
                }
                if (isFinerLoggable)
                {
                   LOG.finer("Sent AUTHDATA");
                }

                // after notifying the server with the authdata, check if the plugin wants to exit
                if (authdl == 0)
                {
                   if (isFinerLoggable)
                   {
                      LOG.finer("Plugin is forcing system exit now.");
                   }
                   System.exit(0);
                }
                else if (authdl == PKT_SIZE_SKIP_TO_NEXT)
                {
                   // user requested skipping authentication for this item
                   if (isFinerLoggable)
                   {
                      LOG.finer("Wait for next item query (if any).");
                   }
                }

                authResult = AUTH_RESULT_NONE;

                if (isFinerLoggable)
                {
                   LOG.finer("Getting AUTHRESULT");
                }

                // receive AUTHRESULT packet
                try
                {
                   authResult = socket.readInt();
                   authAction = socket.readInt();
                }
                catch (IOException ioe)
                {
                   if (isFinerLoggable)
                   {
                      LOG.finer("Failure reading AUTHRESULT " + ioe);
                   }
                   authAction = AUTH_ACTION_ABORT;
                }

                if (isFinerLoggable)
                {
                   LOG.finer("Got AUTHRESULT: " + authResult + " (action = " + authAction + ")");
                }

                if (authAction == AUTH_ACTION_ABORT)
                {
                   if (isFinerLoggable)
                   {
                      LOG.finer("AUTHRESULT forced abort.");
                   }
                   socket.close();
                   return null;
                }
             }

             // if all is fine, then read the subject ID determined on the remote side

             // read the authentication ID, so that it can be saved
             try
             {
                appServer = socket.readUTF();
                if (appServer.isEmpty())
                {
                   appServer = null;
                }
                isProcess = socket.readBoolean();
                remoteAuthID = socket.readUTF();
             }
             catch (IOException ioe)
             {
                if (isFinerLoggable)
                {
                   LOG.finer("Failure reading AUTHRESULT " + ioe);
                }
                authAction = AUTH_ACTION_ABORT;
             }

             if (authAction == AUTH_ACTION_ABORT)
             {
                if (isFinerLoggable)
                {
                   LOG.finer("AUTHRESULT forced abort.");
                }
                socket.close();
                return null;
             }
          }
          finally
          {
             if (auth != null)
                auth.clientFinalize();
          }

          // return the identity used to authenticate
          return new AuthData(isProcess, remoteAuthID, appServer);
       }

       /**
        * Implements the server side of the authentication procedure for
        * processes or interactive clients.  Optionally verifies client's
        * credentials.
        * <p>
        * <i><u>Restricted use</u></i>. This method checks the caller to be
        * <code>com.goldencode.p2j.net.RouterSessionManager$Incoming.run()</code>.
        * <p>
        * There are two major classes of connections: certified and anonymous.
        *
        * @param    socket
        *           Socket identifying this connection.
        *
        * @return   Reference to an object serving as the session key if
        *           authenticated, otherwise either <code>null</code> for failures
        *           or a string describing normal client exit from authentication.
        *
        * @throws   RestrictedUseException
        *           If called improperly.
        */
       public Object authenticateLocal(NetSocket socket)
       throws RestrictedUseException
       {
          // restrict the use of this method
          SecurityUtil.checkCallerAbort("com.goldencode.p2j.net.RouterSessionManager$Incoming.run");

          if (!cfg.isServer())
             return null;

          SecurityCache sc = getCache();

          // safely query cached data
          int    authMode    = sc.getAuthMode();
          int    authRetries = sc.getAuthNumRetries();
          String anonymId    = sc.getAnonymId();
          String hookName    = sc.getHookName();

          // process TLS session certificates
          SSLSession locSess = socket.getSession();
          boolean recvCert = false;
          String alias = null;
          Account[] accs = null;

          java.security.cert.Certificate[] peerCerts = null;
          try
          {
             // if in secure mode
             if (locSess != null)
             {
                // obtain the certs from the other side
                peerCerts = locSess.getPeerCertificates();
             }
          }
          catch (Exception e)
          {
             // exception is handled as if there was 0 certificates received
          }

          if (peerCerts != null && peerCerts.length > 0)
          {
             X509Certificate cert = (X509Certificate)peerCerts[0];
             try
             {
                recvCert = true;
                LOG.finer("Received certificate of " + certificateSm.getSubjectCommonName(cert));
             }
             catch (CertificateParsingException e)
             {
                // this exception can't be tolerated
                return null;
             }
          }

          // deal with TLS connection type
          String localUserId = null;
          String localProcId = null;

          int authModeReq = authMode;
          int authAction  = AUTH_ACTION_CONTINUE;
          int authReqType = -1;

          // the following code will reset the authModeReq to be a return code
          // in any case where the result is actually known without needing to
          // contact the client again, if this is not reset, then it represents
          // the authentication mode which the client must enforce

          if (!recvCert)
          {
             // anonymous connection: the client has to tell the type of
             // the connection. allowed types are:
             // - process
             //   will be accepted if there is an anonymous process account
             // - interactive
             //   the interactive client will be instructed by the server
             //   about the server requested authentication mode
             // - programmatic
             //   not a regular interactive client (web applet for instance);
             //   will be accepted as AUTH_MODE_IDPW but the user account
             //   has to explicitly allow this authentication type AND
             //   some extra protection should be implemented between the web
             //   server and the security manager (one time token alike)
             // - sso
             //   will be accepted if the client uuid and fwd user are known to the server

             LOG.finer((locSess == null) ? "Insecure socket connection."
                                      : "TLS connection without certificate.");

             // get the suggested authentication type from the client
             String requestedUserName = null;
             LOG.finer("Getting AUTHTYPE");
             try
             {
                // read the packet
                authReqType = socket.readInt();
                LOG.finer("Got AUTHTYPE: " + authReqType);

                if (authReqType == AUTH_REQ_SSO)
                {
                   return ssoSm.authenticateLocalSso(socket, sc);
                }

                else if (authReqType == AUTH_REQ_USER)
                {
                   requestedUserName = socket.readUTF();
                   if (requestedUserName.equals(""))
                      requestedUserName = null;

                   if (requestedUserName != null)
                   {
                      Account acc = sc.getAccountById(requestedUserName);
                      if (acc != null && acc instanceof UserAccount)
                      {
                         // effective mode will be used
                         UserAccount userAccount = ((UserAccount) acc);
                         authMode = userAccount.getAuthMode();
                         hookName = userAccount.getAuthPlugin();
                         authModeReq = authMode;
                      }
                   }
                }
             }
             catch (IOException e)
             {
                LOG.log(Level.FINER, "Error getting AUTHTYPE ", e);
                return null;
             }
             LOG.finer("Got userName: " + requestedUserName);

             // verify the suggested authentication type
             if (authReqType == AUTH_REQ_PROCESS)
             {
                // process types are only allowed if there is an associated
                // anonymous process id that is defined
                if (anonymId == null)
                {
                   authModeReq = AUTH_RESULT_UNSPECIFIED_FAILURE;
                   authAction  = AUTH_ACTION_ABORT;
                }
                else
                {
                   authModeReq = AUTH_RESULT_SUCCESS;
                   authAction  = AUTH_ACTION_DONE;
                   localProcId = anonymId;
                }
             }
             else if (authReqType == AUTH_REQ_USER)
             {
                // this is the regular interactive user over an anonymous link fall-through
             }
             else if (authReqType == AUTH_REQ_PROGRAM)
             {
                // programmatic interactive (applet) access
                authMode    = AUTH_MODE_IDPW;
                authModeReq = AUTH_MODE_IDPW;
             }
             else
             {
                // unknown authentication type
                authModeReq = AUTH_RESULT_UNSPECIFIED_FAILURE;
                authAction  = AUTH_ACTION_ABORT;
             }
          }
          else
          {
             // certified connection, identify the account by certificate and
             // figure out truststore alias of the certificate
             try
             {
                alias = sc.getTrustStore().getCertificateAlias(peerCerts[0]);
             }
             catch (KeyStoreException e)
             {
                // nothing to do
             }

             if (alias == null)
             {
                LOG.finer("Can't identify the alias");
                authModeReq = AUTH_RESULT_UNSPECIFIED_FAILURE;
                authAction  = AUTH_ACTION_ABORT;
             }
             else
             {
                LOG.finer("Certificate has alias <" + alias + ">");

                int procs = sc.getCountByAlias(alias, Account.ACC_PROCESS);
                int users = sc.getCountByAlias(alias, Account.ACC_USER);
                LOG.finer("Associated process accounts: " + procs);
                LOG.finer("Associated user    accounts: " + users);

                if (procs + users == 0)
                {
                   LOG.finer("No account for alias <" + alias + ">");
                   authModeReq = AUTH_RESULT_UNSPECIFIED_FAILURE;
                   authAction  = AUTH_ACTION_ABORT;
                }
                else
                {
                   if (procs > 0 && users > 0)
                   {
                      LOG.finer("Mixed user and process accounts for alias <" + alias + ">");
                      authModeReq = AUTH_RESULT_UNSPECIFIED_FAILURE;
                      authAction  = AUTH_ACTION_ABORT;
                   }
                   else
                   {
                      if (procs > 0)
                      {
                         // processes only
                         if (procs > 1)
                         {
                            LOG.finer("Ambiguous association of process accounts");
                            authModeReq = AUTH_RESULT_UNSPECIFIED_FAILURE;
                            authAction  = AUTH_ACTION_ABORT;
                         }
                         else
                         {
                            accs = sc.getAccountsByAlias(alias, Account.ACC_PROCESS);
                            localProcId = accs[0].getSubjectId();
                            LOG.finer("Associated process account is <" + localProcId + ">");
                            authModeReq = AUTH_RESULT_SUCCESS;
                            authAction  = AUTH_ACTION_DONE;
                         }
                      }
                      else
                      {
                         // users only, this is a table of valid user IDs
                         accs = sc.getAccountsByAlias(alias, Account.ACC_USER);
                         if (accs != null && accs.length == 1)
                         {
                            // effective mode will be used
                            authModeReq = ((UserAccount) accs[0]).getAuthMode();
                         }
                      }
                   }
                }
             }
          }

          String serverHookName = hookName;
          String clientHookName = hookName;
          byte[] clientHookClassBytes = SecurityUtil.getClassBytes(hookName);
          if (clientHookClassBytes == null)
          {
             // hook probably consists of separate client- and  server-side classes
             LOG.finer("no hook class for " + clientHookName);
             clientHookName = hookName + "Client";
             clientHookClassBytes = SecurityUtil.getClassBytes(clientHookName);
             if (clientHookClassBytes == null)
             {
                LOG.finer("no hook class for " + clientHookName);
                clientHookName = hookName;
             }
             else
             {
                LOG.finer("hook class is " + clientHookName);
                serverHookName = hookName + "Server";

                if (!SecurityUtil.classExists(serverHookName))
                {
                   // use the server's default processing as there is
                   // no user-replaceable server-side plugin
                   serverHookName = null;
                }
             }
          }
          else
          {
             LOG.finer("hook class is " + clientHookName);
          }

          Authenticator serverAuth = null;
          if (hookName != null)
          {
             try
             {
                Class<?> server = Class.forName(serverHookName);
                serverAuth = (Authenticator) server.getDeclaredConstructor().newInstance();
                serverAuth.configure(getHookParam(sc, hookName));
             }
             catch (Exception exc)
             {
                // can't find or can't use the user-specified replacement, so
                // use the default approach
                serverAuth = serverAuthenticator;
             }
          }
          else
          {
             // no user-specified replacement, so use the default approach
             serverAuth = serverAuthenticator;
          }

          // retryable section of the authentication protocol
          int authResult = AUTH_RESULT_NONE;
          int authCrtRetries = ALWAYS_RETRY;

          Set<String> authEntities;
          try
          {
             authEntities = serverAuth.getAuthenticationEntities();
             if (authEntities == null)
             {
                authEntities = new HashSet<>();
                authEntities.add("");      // dummy entity to force iterate exactly once
             }
             if (authEntities.isEmpty())
             {
                // there are no declared entities for authentication ?
                // check if we can authenticate using configuration parameter
                localUserId = getHookParam(sc, hookName);
             }
          }
          catch (RuntimeException rte)
          {
             // there was an error while enumerating the entities. Disconnect the client.
             try
             {
                LOG.finer("Sending special AUTHMODEREQ");
                socket.writeInt(authModeReq);
                socket.writeInt(AUTH_ACTION_ABORT);
                socket.flush();
             }
             catch (IOException e)
             {
                // ignore any other io-exception
             }

             return rte.getMessage();
          }

          // iterate each declared entities and allow authRetries retries for authentication
          // (some ?) entities can be skipped but if authentication fails authRetries in a row
          // the client will be be disconnected no matter how many entities have previously
          // ended with success
          for (String entity : authEntities)
          {
             authCrtRetries = authRetries;
             authAction = AUTH_ACTION_CONTINUE;
             authResult = AUTH_RESULT_NONE;       // force re-send the new database to client

             do
             {
                // interpreting AUTHMODE
                switch (authModeReq)
                {
                   case AUTH_MODE_X509:
                      // this mode means the client's certificate is required
                      if (!recvCert)
                      {
                         // certificate required but not received
                         authModeReq = AUTH_RESULT_UNSPECIFIED_FAILURE;
                         authAction  = AUTH_ACTION_ABORT;
                         break;
                      }
                      // The user certificate has been identified. One or more
                      // accounts are selected. This mode required an unambiguous
                      // association of certificate (alias) to an user account. If
                      // only one account has been selected, it gets used. Otherwise,
                      // the set has to include a special default account where
                      // subjectID == alias.
                      if (accs.length > 1)
                      {
                         int i;
                         for (i = 0; i < accs.length; i++)
                         {
                            String subid = accs[i].getSubjectId();
                            if (subid.compareToIgnoreCase(alias) == 0)
                            {
                               localUserId = accs[i].getSubjectId();
                               authModeReq = AUTH_RESULT_SUCCESS;
                               authAction  = AUTH_ACTION_DONE;
                               break;
                            }
                         }
                         if (i == accs.length)
                         {
                            LOG.finer("Ambiguous association of user accounts");
                            authModeReq = AUTH_RESULT_UNSPECIFIED_FAILURE;
                            authAction  = AUTH_ACTION_ABORT;
                         }
                      }
                      else
                      {
                         localUserId = accs[0].getSubjectId();
                         authModeReq = AUTH_RESULT_SUCCESS;
                         authAction  = AUTH_ACTION_DONE;
                      }
                      break;

                   case AUTH_MODE_IDPW:
                      // This mode means that the server wants ID and password and
                      // does not care about client's certificate whether it was sent
                      // or not. Drop all preselected accounts.
                      accs = null;
                      authModeReq = authMode;
                      break;

                   case AUTH_MODE_X509_IDPW:
                      // This mode means that the server wants ID and password as
                      // well as the client's certificate which should have been sent.
                      if (!recvCert)
                      {
                         // certificate required but not received
                         authModeReq = AUTH_RESULT_UNSPECIFIED_FAILURE;
                         authAction  = AUTH_ACTION_ABORT;
                         break;
                      }
                      // The user certificate has been identified. One or more
                      // accounts were preselected. The user sends a preferred ID with password.
                      authModeReq = authMode;
                      break;

                   case AUTH_MODE_CUSTOM:
                      // This mode means that the server runs custom authentication hook regardless.
                      authModeReq = authMode;
                      break;
                }

                // Note: authModeReq is a little abused. Multiple semantically different items are
                // stored in it and the protocol probably works just because of the particular case
                // how the constants are initialized.
                // Explicitly: authModeReq is declared and first initialized as a AUTH_MODE_ holder
                // but then assigned also to AUTH_RESULT_ constants (semantic types are different).
                // Probably this is a case of re-usage of the variable of same type (int) for two
                // different purposes.
                if (authModeReq != AUTH_RESULT_SUCCESS)
                {
                   LOG.finer("Sending AUTHMODEREQ: " + authModeReq + " (action = " + authAction + ")");

                   // send the AUTHMODEREQ packet to the client
                   try
                   {
                      // send the packet
                      socket.writeInt(authModeReq);
                      socket.writeInt(authAction);
                      socket.flush();
                   }
                   catch (SocketException se)
                   {
                      LOG.log(Level.FINER, "Error sending AUTHMODEREQ ", se);
                      return null;
                   }
                   catch (IOException ioe)
                   {
                      LOG.log(Level.FINER, "Error sending AUTHMODEREQ ", ioe);
                      return null;
                   }

                   LOG.finer("Sent AUTHMODEREQ");

                   // only send the class down the first time
                   if (authModeReq == AUTH_MODE_CUSTOM && authResult == AUTH_RESULT_NONE)
                   {
                      int csize = (clientHookClassBytes == null) ? 0 : clientHookClassBytes.length;

                      // send AUTHCUSTOM packet
                      LOG.finer("Sending AUTHCUSTOM");
                      try
                      {
                         // the packet is made of three pieces:
                         // 1 - byte array, copy of the class to run
                         //     formatted as int + bytes, where int == 0 means no class
                         // 2 - fully qualified class name formatted as UTF
                         // 3 - string of class parameters formatted as UTF

                         // write piece 1 - may be empty
                         socket.writeInt(csize);
                         if (csize > 0)
                         {
                            socket.writeBytes(clientHookClassBytes);
                         }

                         // write piece 2 - class name
                         socket.writeUTF(clientHookName);

                         // write piece 3 - class parameters / processed entity
                         socket.writeUTF(entity.isEmpty() ? getHookParam(sc, hookName) : entity);
                         socket.flush();
                      }
                      catch (SocketException se)
                      {
                         LOG.log(Level.FINER, "Error sending AUTHCUSTOM ", se);
                         return null;
                      }
                      catch (IOException ioe)
                      {
                         LOG.log(Level.FINER, "Error sending AUTHCUSTOM ", ioe);
                         return null;
                      }
                      LOG.finer("Sent AUTHCUSTOM");
                   }

                   if (authAction == AUTH_ACTION_ABORT)
                   {
                      return null;
                   }

                   byte authData[] = null;
                   int  authSize = 0;

                   LOG.finer("Getting AUTHDATA");

                   // get authentication data from the client
                   try
                   {
                      authSize = socket.readInt();
                      LOG.finer("Getting AUTHDATA authSize=" + authSize);

                      if (authSize == 0)
                      {
                         // special case - client want to exit authentication cleanly
                         return "Client decided to quit authentication";
                      }
                      else if (authSize == PKT_SIZE_SKIP_TO_NEXT)
                      {
                         // this is the particular case when the entity authentication is skipped
                         // there are other entities that need authentication
                         authData = null;
                         authResult = AUTH_RESULT_SKIP_TO_NEXT;
                      }
                      else if (authSize > 0)
                      {
                         authData = new byte[authSize];
                         socket.readBytes(authData);
                      }
                      else
                      {
                         LOG.finer("Invalid protocol. Error getting AUTHDATA.");
                         return null;
                      }
                   }
                   catch (IOException ioe)
                   {
                      LOG.log(Level.FINER, "Error getting AUTHDATA " + ioe);
                      return null;
                   }
                   LOG.finer("Got AUTHDATA");

                   AuthenticationResponse response = null;
                   authResult = AUTH_RESULT_NONE;

                   // process authentication data
                   switch (authMode)
                   {
                      case AUTH_MODE_IDPW:
                      case AUTH_MODE_X509_IDPW:
                         response    = serverAuthenticator.serverAuthHook(authData, "");
                         localUserId = response.getSubject();
                         authResult  = response.getReturnCode();

                         if (authResult != AUTH_RESULT_SUCCESS)
                         {
                            break;
                         }

                         // for programmatic call, check the account for matching
                         // authentication type
                         if (authReqType == AUTH_REQ_PROGRAM)
                         {
                            UserAccount user = (UserAccount) sc.getAccountById(localUserId);
                            if (user.getAuthMode() != AUTH_MODE_IDPW)
                            {
                               LOG.finer("No programmatic use for ID <" + localUserId + ">");
                               localUserId = null;
                            }
                         }
                         // check the requested ID against the list of allowed IDs
                         else if (accs != null)
                         {
                            int i;

                            for (i = 0; i < accs.length; i++)
                            {
                               if (accs[i].getSubjectId().compareToIgnoreCase(localUserId) == 0)
                                  break;
                            }

                            if (i == accs.length)
                            {
                               LOG.finer("Not allowed to use account <" + localUserId + ">");
                               localUserId = null;
                            }
                         }
                         break;

                      case AUTH_MODE_CUSTOM:
                         // this mode means that the server runs custom auth hook
                         response = runCustomServerHook(sc,
                                                        hookName,
                                                        serverAuth,
                                                        entity,
                                                        authData);
                         localUserId = response.getSubject();
                         authResult  = response.getReturnCode();
                         break;
                   }

                   // send the negative AUTHRESULT packet to the client
                   if (localUserId == null)
                   {
                      authAction = AUTH_ACTION_ABORT;

                      if (authCrtRetries == ALWAYS_RETRY || authCrtRetries > 0)
                      {
                         authAction = AUTH_ACTION_RETRY;

                         if (authCrtRetries > 0)
                         {
                            authCrtRetries--;
                         }
                      }
                         else if (authCrtRetries == 0)
                      {
                         authAction = AUTH_ACTION_RETRY; // AUTH_ACTION_CONTINUE;

                         if (authResult != AUTH_RESULT_SKIP_TO_NEXT && // user pressed Cancel
                             authResult != AUTH_RESULT_SUCCESS)       // successfully authenticated
                         {
                            // userid/password failed 3rd time, bail out the client
                            authAction = AUTH_ACTION_ABORT;
                         }
                         else
                         {
                            // force client to reread the new database
                            authResult = AUTH_RESULT_NONE;
                         }
                      }

                      if (!sendAuthResult(socket, authResult, authAction))
                      {
                         return null;
                      }

                      if (authAction == AUTH_ACTION_ABORT)
                      {
                         return null;
                      }
                   }
                   else
                   {
                      authAction = AUTH_ACTION_DONE;
                   }
                }

                // reset to the default expected mode
                authModeReq = authMode;

             } while (authAction == AUTH_ACTION_RETRY);
          } // end for each entity

          String appServer = null;

          Object res[] = postAuthenticateWorker(socket, localProcId, localUserId, true);
          authResult = (int) res[0];
          authAction = (int) res[1];
          appServer = (String) res[2];
          SecurityContext newContext = (SecurityContext) res[3];
          SecuritySession sess = (SecuritySession) res[4];

          boolean decision = authResult == AUTH_RESULT_SUCCESS;

          // send the AUTHRESULT packet to the client
          if (!sendAuthResult(socket, authResult, authAction))
          {
             // error sending the result; do not keep the session
             decision = false;
          }
          else if (decision)
          {
             // send the account used for authentication back to the requester
             // so that it can save it.
             LOG.finer("Sending LOCALUSERID: " + localUserId);

             // send the LOCALUSERID packet to the client
             try
             {
                // send the packet
                socket.writeUTF(appServer == null ? "" : appServer);
                socket.writeBoolean(localUserId == null && localProcId != null);
                socket.writeUTF(localUserId != null ? localUserId : localProcId);
                socket.flush();
             }
             catch (SocketException se)
             {
                LOG.log(Level.FINER, "Error sending LOCALUSERID ", se);
                decision = false;
             }
             catch (IOException ioe)
             {
                LOG.log(Level.FINER, "Error sending LOCALUSERID ", ioe);
                decision = false;
             }
          }

          if (!decision)
          {
             if (sess != null)
             {
                sessionSm.removeSessionWorker(sess);
             }
             return null;
          }

          // if all goes well, set the p2j.net.SessionListener into socket (if any):
          if (serverAuth != null)
          {
             socket.addSessionListener(serverAuth.getSessionListener());
          }

          // this is the only successful exit point from this method
          return newContext;
       }

       /**
        * Authenticate the current in-JVM thread using the configured certificate.
        *
        * @param    cfg
        *           The configuration.
        *
        * @return   The newly created context or <code>null</code> if authentication failed.
        *
        * @throws   RestrictedUseException
        *           If this method is called from restricted context
        */
       public Object authenticateServer(BootstrapConfig cfg)
       throws RestrictedUseException
       {
          if (!this.cfg.isServer())
          {
             throw new IllegalStateException("the method must be called from server JVM.");
          }

          boolean secure = cfg.getBoolean("net", "connection", "secure", false);

          if (!secure)
          {
             return null;
          }

          if (SecurityContextStack.getContext() != null)
          {
             throw new IllegalStateException("Context already set for this thread.");
          }

          contextSm.setUniqueInitialSecurityContext();

          SecurityCache sc = getCache();

          // safely query cached data
          int    authAction  = AUTH_ACTION_CONTINUE;
          Account[] accs = null;
          String localUserId = null;
          String localProcId = null;

          String alias = null;
          Certificate cert = null;

          try
          {
             alias = TransportSecurity.getSubjectAlias(cfg, false);
             if (alias == null)
             {
                LOG.finer("X509 certificate attempted, no user/process alias provided.");
                return null;
             }

             KeyStore ks = TransportSecurity.loadKeyStore(cfg, false);

             cert = ks.getCertificate(alias);
          }
          catch (Exception e)
          {
             String msg = "Could not load the " + alias + " certificate from the provided keystore";
             LOG.logp(Level.SEVERE, "SecurityManager", "authenticateServer", msg + "\n" + e);
             return null;
          }

          if (!(cert instanceof X509Certificate))
          {
             String msg = "Could not load the " + alias + " certificate from the provided keystore";
             LOG.logp(Level.SEVERE, "SecurityManager", "authenticateServer", msg);
             return null;
          }

          X509Certificate x509Cert = (X509Certificate) cert;
          try
          {
             LOG.finer("Received certificate of " + certificateSm.getSubjectCommonName(x509Cert));
          }
          catch (CertificateParsingException e)
          {
             // this exception can't be tolerated
             return null;
          }

          LOG.finer("Certificate has alias <" + alias + ">");

          int procs = sc.getCountByAlias(alias, Account.ACC_PROCESS);
          int users = sc.getCountByAlias(alias, Account.ACC_USER);
          LOG.finer("Associated process accounts: " + procs);
          LOG.finer("Associated user    accounts: " + users);

          if (procs + users == 0)
          {
             LOG.finer("No account for alias <" + alias + ">");
             authAction = AUTH_ACTION_ABORT;
          }
          else
          {
             if (procs > 0 && users > 0)
             {
                LOG.finer("Mixed user and process accounts for alias <" + alias + ">");
                authAction = AUTH_ACTION_ABORT;
             }
             else
             {
                if (procs > 0)
                {
                   // processes only
                   if (procs > 1)
                   {
                      LOG.finer("Ambiguous association of process accounts");
                      authAction = AUTH_ACTION_ABORT;
                   }
                   else
                   {
                      accs = sc.getAccountsByAlias(alias, Account.ACC_PROCESS);
                      localProcId = accs[0].getSubjectId();
                      LOG.finer("Associated process account is <" + localProcId + ">");
                      authAction = AUTH_ACTION_DONE;
                   }
                }
                else
                {
                   // users only, this is a table of valid user IDs
                   accs = sc.getAccountsByAlias(alias, Account.ACC_USER);
                   if (accs != null && accs.length == 1)
                   {
                      // effective mode will be used
                      int authMode = ((UserAccount) accs[0]).getAuthMode();
                      if (authMode != AUTH_MODE_X509)
                      {
                         // only X509 certificate auth is allowed by this API.
                         String msg = "Only certificate authentication is allowed by this API";
                         LOG.logp(Level.SEVERE, "SecurityManager", "authenticateServer", msg);
                      }
                   }
                }
             }
          }

          if (authAction == AUTH_ACTION_ABORT)
          {
             return null;
          }
          else if (authAction == AUTH_ACTION_CONTINUE)
          {
             // The user certificate has been identified. One or more
             // accounts are selected. This mode required an unambiguous
             // association of certificate (alias) to an user account. If
             // only one account has been selected, it gets used. Otherwise,
             // the set has to include a special default account where
             // subjectID == alias.
             if (accs.length > 1)
             {
                int i;
                for (i = 0; i < accs.length; i++)
                {
                   String subid = accs[i].getSubjectId();
                   if (subid.compareToIgnoreCase(alias) == 0)
                   {
                      localUserId = accs[i].getSubjectId();
                      authAction  = AUTH_ACTION_DONE;
                      break;
                   }
                }
                if (i == accs.length)
                {
                   LOG.finer("Ambiguous association of user accounts");
                   authAction  = AUTH_ACTION_ABORT;
                }
             }
             else
             {
                localUserId = accs[0].getSubjectId();
                authAction  = AUTH_ACTION_DONE;
             }
          }

          if (authAction == AUTH_ACTION_ABORT)
          {
             return null;
          }

          Object sessionId = cfg.getString("net", "session", "id", null);
          if (sessionId == null)
          {
             sessionId = UUID.randomUUID().toString();
          }
          Object res[] = postAuthenticateWorker(sessionId, localProcId, localUserId, false);

          int authResult = (int) res[0];
          authAction = (int) res[1];
          String appServer = (String) res[2];
          SecurityContext newContext = (SecurityContext) res[3];
          SecuritySession sess = (SecuritySession) res[4];

          boolean decision = authResult == AUTH_RESULT_SUCCESS;

          if (decision)
          {
             // push this context for this thread
             contextSm.pushContextWorker(newContext);
          }

          return decision ? newContext : null;
       }

       /**
        * After this call the initial security context of the admin server will be established.
        *
        * @param    sessionId
        *           The session id
        * @param    userid
        *           The user id
        * @param    password
        *           The password
        *
        * @return   The code of the authorization result
        *
        * @throws   RestrictedUseException
        *           If this method is called from restricted context
        */
       public int authenticateServer(Object sessionId, String userid, String password)
       throws RestrictedUseException
       {
          // restrict the use of this method
          SecurityUtil.checkCallerAbort("com.goldencode.p2j.admin.server.AuthServiceImpl.login");

          if (!cfg.isServer())
          {
             throw new IllegalStateException("the method must be called from server");
          }

          try
          {
             contextSm.setUniqueInitialSecurityContext();
             AuthenticationResponse res =
                serverAuthenticator.serverAuthHook(SecurityUtil.packageIdPassword(userid, password), null);

             String localUserId = res.getSubject();
             int authResult  = res.getReturnCode();

             if (authResult != AUTH_RESULT_SUCCESS)
             {
                return authResult;
             }

             Object postRes[] = postAuthenticateWorker(sessionId, null, localUserId, false);
             return (int) postRes[0];
          }
          catch (RestrictedUseException e)
          {
             throw new RuntimeException(e);
          }
       }

       /**
        * Returns the OS username and password associated with FWD username. If fwd user has attribute osUser,
        * try to locate the OS user in directory. OS user nodes can have an optional attr `password`.
        *
        * @param    fwdUsername
        *           The FWD account.
        *
        * @return   Null or string array containing OS user at first position and OS pass at second.
        */
       public String[] getOsUserCredByFwdAcc(String fwdUsername)
       {
          SecurityCache sc = getCache();
          int userI = sc.getOrdinalById(fwdUsername);
          if (userI == -1)
          {
             return null;
          }

          UserAccount userAccount = (UserAccount) sc.getAccountByOrd(userI);
          String osUser = userAccount.getOsUser();

          return StringHelper.hasContent(osUser) ? getOsUserCredByName(osUser) : null;
       }

       /**
        * Returns the OS username and password associated with FWD username. If fwd user has attribute osUser,
        * try to locate the OS user in directory. OS user nodes can have an optional attr `password`.
        *
        * @param    osUser
        *           The OS username.
        *
        * @return   Null or string array containing OS user at first position and OS pass at second.
        */
       public String[] getOsUserCredByName(String osUser)
       {
          OsUserAccount osAccount = getCache().getOsUserById(osUser);
          if (osAccount == null)
          {
             return null;
          }
          String osPass = osAccount.getPassword() != null ?
             new String(Base64.base64ToByteArray(new String(osAccount.getPassword()))) :
             null;
          return new String[]{ osAccount.getSubjectId(), osPass };
       }

       /**
        * Reads the directory node where the OS user details are supposed to reside. If valid osuser node is not
        * found, returns null. Otherwise, returns an array with user ID at position 0 and user pass at position 1.
        *
        * @param    ds
        *           The directory service.
        * @param    parentNodeId
        *           The parent directory node.
        * @param    osUser
        *           The OS user ID.
        *
        * @return   Null, if the OS user not found, or string array containing OS user ID at first position and
        *           OS user pass at second.
        */
       private String[] getOsUserPassFromNode(DirectoryService ds, String parentNodeId, String osUser)
       {
          String osUserNodeId = parentNodeId + "/" + osUser;
          String osUserNodeClass = ds.getNodeClass(osUserNodeId);
          if (osUserNodeClass == null || !osUserNodeClass.endsWith("/osuser"))
          {
             return null;
          }

          String osPass = null;
          Attribute[] osUserAttrs = ds.getNodeAttributes(osUserNodeId);
          if (osUserAttrs != null && osUserAttrs.length > 0)
          {
             for (int j = 0; j < osUserAttrs.length; j++)
             {
                Attribute osUserAttr = osUserAttrs[j];
                if ("password".equals(osUserAttr.getName()))
                {
                   byte[] osPassByteArray = (byte[]) osUserAttr.getValue(0);
                   osPass = new String(Base64.base64ToByteArray(new String(osPassByteArray)));
                   break;
                }
             }
          }
          return new String[] { osUser, osPass };
       }

       /**
        * Get the server account ID.
        *
        * @return    See above.
        */
       public String getServerId()
       {
          SecurityContextStack ctx = SecurityContextStack.getContext();
          SecurityContext key = ctx.getSessionKey();

          return key.getCache().getServerAccount().getSubjectId();
       }

       /**
        * Returns the subject ID associated with the current context.
        * <p>
        * The subject ID returned is either userID, or processId, if no user is
        * logged on.
        *
        * @return subject ID or "" if nothing is established
        */
       public String getUserId()
       {
          // Pick up the generation of security cache
          SecurityContextStack ctxs = SecurityContextStack.getContext();
          SecurityContext ctx = ctxs.getEffectiveContext();
          SecurityCache sc = ctx.getCache();

          // figure out subject ordinal
          int userOrd = ctx.getUserOrdinal();
          if (userOrd == -1)
          {
             userOrd = ctx.getProcessOrdinal();
          }
          if (userOrd == -1)
          {
             return "";
          }

          // return the subject's identity
          Account account = sc.getAccountByOrd(userOrd);
          if (account.getAccountType() == Account.ACC_PROCESS)
          {
            return account.getSubjectId();
          }
          else
          {
             StringBuilder sb = new StringBuilder(account.getSubjectId());
             String storageId = SessionUtils.getStorageId();
             if (storageId != null)
             {
                sb.append("/").append(storageId);
             }

             return sb.toString();
          }
       }

       /**
        * Returns the the appserver associated with the current context.
        * <p>
        * If no appserver is associated with the current context, it returns <code>null</code>
        *
        * @return   See above.
        */
       public String getAppServer()
       {
          // Pick up the generation of security cache
          SecurityContextStack ctxs = SecurityContextStack.getContext();
          SecurityContext ctx = ctxs.getEffectiveContext();
          SecurityCache sc = ctx.getCache();

          // figure out subject ordinal
          int userOrd = ctx.getUserOrdinal();
          if (userOrd == -1)
          {
             userOrd = ctx.getProcessOrdinal();
          }
          if (userOrd == -1)
          {
             return null;
          }

          // return the associated appserver
          Account acc = sc.getAccountByOrd(userOrd);
          return (acc instanceof ProcessAccount) ? ((ProcessAccount) acc).getAppServer() : null;
       }

       /**
        * Get the peer (remote) host for this session.
        *
        * @return   See above.
        */
       public String getPeerHost()
       {
          NetSocket nsock = getPeerSocket();
          InetSocketAddress peer  = nsock.getRemoteSockAddr();

          return peer.getHostName();
       }

       /**
        * Get the peer (remote) port for this session.
        *
        * @return   See above.
        */
       public int getPeerPort()
       {
          NetSocket nsock = getPeerSocket();
          InetSocketAddress peer = nsock.getRemoteSockAddr();

          return peer.getPort();
       }

       /**
        * Get the certificate alias for the running sever.
        *
        * @return   The server's alias or <code>null</code> if not called on a server thread.
        */
       public String getServerAlias()
       {
          if (!isServerAccount())
          {
             return null;
          }

          return getCache().getServerAccount().getAlias();
       }

       /**
        * Get the certificate alias for the web sever.
        *
        * @return   The web server's alias or <code>null</code> if not called on a server thread.
        */
       public String getWebServerAlias()
       {
          if (!isServerAccount())
          {
             return null;
          }

          return getCache().getWebServerAlias();
       }

       /**
        * Get the certificate alias for the specified subject. May be <code>null</code>.
        *
        * @return   See above.
        */
       public String getAccountAlias(String subjectId)
       {
          Account acc = getCache().getAccountById(subjectId);

          return (acc == null ? null : acc.getAlias());
       }

       /**
        * Get the system user used to authenticate this subjectId on the machine running the P2J
        * server.  The search is done using the subject ID, not the context's subject; both the
        * per-server and per-account configurations are searched (using {@link DirScope#BOTH}.
        * <p>
        * If none configured, default to the subject ID.
        *
        * @param    subjectId
        *           The subject ID.
        *
        * @return   See above.
        */
       public String getSystemUser(String subjectId)
       {
          if (!isServerAccount())
          {
             return null;
          }

          String[] accountIds = getAccountIds(subjectId);

          return ConfigItem.SYSTEM_USER.read(null, DirScope.BOTH, accountIds);
       }

       /**
        * Get the system user password used to authenticate this subjectId on the machine running
        * the P2J server. The search is done using the subject ID, not the context's subject; both
        * the per-server and per-account configurations are searched (using {@link DirScope#BOTH}.
        * <p>
        * If none configured, default to <code>null</code>.
        *
        * @param    subjectId
        *           The subject ID.
        *
        * @return   See above.
        */
       public String getSystemPassword(String subjectId)
       {
          if (!isServerAccount())
          {
             return null;
          }

          String[] accountIds = getAccountIds(subjectId);

          return ConfigItem.SYSTEM_PASS.read(null, DirScope.BOTH, accountIds);
       }

       /**
        * Get the identifier mapped to the current thread.  This is only
        * meaningful for a given unit of work, since threads generally are
        * recycled into a thread pool (and their thread ID unmapped) when their
        * unit of work is done.
        *
        * @return  Current thread's ID.
        */
       public Integer getThreadId()
       {
          return THREAD_ID.get();
       }

       /**
        * Get all the subject IDs associated with the given P2J account.  For user accounts, this
        * includes the passed account and any associated groups.  For all others, just return the
        * passed account.
        *
        * @param    acct
        *           The P2J subject ID.
        *
        * @return   A list with all the associated subject IDs.
        */
       public String[] getAccountIds(String acct)
       {
          if (!isServerAccount())
          {
             return new String[0];
          }

          ProcessAccount pc = getCache().getServerAccount();
          if (!pc.isMaster())
          {
             return new String[0];
          }

          Account acc = getAccount(acct);

          if (acc instanceof UserAccount)
          {
             int[] gids = ((UserAccount) acc).getGroups();
             String[] accts = new String[gids.length + 1];
             accts[0] = acct;

             for (int i = 1; i < accts.length; i++)
             {
                accts[i] = getCache().getAccountByOrd(gids[i - 1]).getSubjectId();
             }

             return accts;
          }

          return new String[] { acct };
       }

       /**
        * Returns ALL subject IDs associated with the current context.
        * <p>
        * The first subject ID in the array is either a userID or a processId
        * (if no user is logged on).  All subsequent subject IDs are the list
        * of group names.  Note that as long as there is a security context,
        * there will be at least 1 subject ID.  The group subject IDs will only
        * be there if the user or process belongs to 1 or more groups.
        *
        * @return   The array of subject IDs or an empty array (length == 0) if
        *           no security context exists.
        */
       public String[] getAccountIds()
       {
          // find the security cache
          SecurityContextStack ctxs = SecurityContextStack.getContext();
          SecurityContext      ctx  = ctxs.getEffectiveContext();
          SecurityCache        sc   = ctx.getCache();

          // result list (initially empty)
          String[] list = new String[0];

          int[] groupList = null;

          // figure out 1st subject ordinal
          int ord = ctx.getUserOrdinal();

          if (ord == -1)
          {
             // we aren't a user, perhaps we are a process?
             ord = ctx.getProcessOrdinal();
          }
          else
          {
             UserAccount ua = (UserAccount) sc.getAccountByOrd(ord);
             groupList = ua.getGroups();
          }

          if (ord != -1)
          {
             // we have at least 1 subject ID to return, store it
             ArrayList<String> array = new ArrayList<>();
             array.add(sc.getAccountByOrd(ord).getSubjectId());

             // add groups
             if (groupList != null)
             {
                for (int aGroupList : groupList)
                {
                   array.add(sc.getAccountByOrd(aGroupList).getSubjectId());
                }
             }

             // convert to an array
             list = array.toArray(list);
          }

          // return the subject list
          return list;
       }

       /**
        * Verifies peer's credentials and performs remote authentication.
        * <p>
        * <i><u>Restricted use</u></i>. This method checks the caller to be
        * <code>com.goldencode.p2j.net.RouterSessionManager.authenticateRemote()</code>.
        *
        * @param  identity
        *         remote application/client identity in the form "processID/userID"
        *
        * @return reference to an object serving as a key if authenticated,
        *         otherwise <code>null</code>
        *
        * @throws RestrictedUseException if called improperly
        */
       public Object authenticateRemote(String identity)
       throws RestrictedUseException
       {
          // restrict the use of this method
          SecurityUtil.checkCallerAbort("com.goldencode.p2j.net.RouterSessionManager.authenticateRemote");

          if (!cfg.isServer())
             return null;

          // safely query cached data
          SecurityCache sc = getCache();

          // parse the identity
          String[] parts = identity.split("/");
          int len = parts.length;
          if (len < 1 || len > 2)
             return null;

          String procID = parts[0];
          String userID = (len == 2) ? parts[1] : null;

          // create new security context
          SecurityContext newContext = contextSm.createSecurityContext(sc, procID, userID);

          if (newContext == null || sc != getCache())
          {
             LOG.finer("authenticateRemote: newer security cache generation available; " +
                    "authentication aborted.");
             return null;
          }

          // create new session
          sessionSm.addSession(identity, null, sc, newContext);
          contextSm.assignContext(newContext);

          return newContext;
       }

       /**
        * Searches abstract resource registry for a plugin that is responsible
        * for the resources of the specified type.
        * <p>
        * Note that the security cache has to be taken from the security context
        * of the thread, since in the current cache the resource plugin can be
        * either registered under different ID or even be dropped.
        *
        * @param resourceTypeName
        *        resource type name
        *
        * @return <code>AbstractResource</code> object
        */
       public AbstractResource getPluginInstance(String resourceTypeName)
       {
          if (!cfg.isServer())
             return null;

          // Pick up the generation of the security cache
          SecurityContextStack scs = SecurityContextStack.getContext();
          if (scs == null)
             return null;
          SecurityCache sc = scs.getCache();

          int sysOrdinal = sc.getRegistryOrdByTypeName(resourceTypeName);
          if (sysOrdinal == -1)
             return null;
          else
             return sc.getRegistryByOrd(sysOrdinal).getPlugin();
       }

       /**
        * Searches abstract resource registry for a plugin that is responsible
        * for the resources of the specified type.
        * <p>
        * Note that the security cache has to be taken from the security context
        * of the thread, since in the current cache the resource plugin can be
        * either registered under different ID or even be dropped.
        *
        * @param resourceTypeName
        *        resource type name
        *
        * @return <code>AbstractResource</code> object, never {@code null}.
        *
        * @throws IllegalStateException
        *         when the requested plugin is not available.
        */
       public AbstractResource getMandatoryPluginInstance(String resourceTypeName)
       {
          AbstractResource plugin = getPluginInstance(resourceTypeName);
          if (plugin == null)
          {
             throw new IllegalStateException("Server-side file system resource plugin is not configured! " +
                                                "Please configure the plugin in the server directory and " +
                                                "restart the server.");
          }

          return plugin;
       }

       /**
        * Searches the cache for a previously made decision.
        * <p>
        * Plugins call this method to check whether a cached access check decision
        * is available.  Cached access check decisions are identified by triplets
        * of {resourceId, instanceName, mode} exactly as they were specified when
        * the decision was taken.
        * <p>
        * Note that the security cache has to be taken from the security context
        * of the thread.
        *
        * @param  resourceId
        *         resource identity
        *
        * @param  instanceName
        *         resource instance name
        *
        * @param  mode
        *         access mode (requested rights) the decision was made about
        * @return <code>Boolean</code> that wraps a decision, or <code>null</code>
        *         if not available.
        */
       public Boolean getCachedDecision(int resourceId, String instanceName, int mode)
       {
          if (!cfg.isServer())
             return null;

          SecurityContextStack ctx = SecurityContextStack.getContext();

          // Pick up the generation of the security cache
          SecurityCache sc = ctx.getCache();
          int nres = sc.getNumResources();

          if (resourceId < 0 || resourceId >= nres)
             return null;

          Decision d = ctx.getDecisionCache(resourceId);
          Boolean found = d.search(instanceName, mode);

          if (found != null)
          {
             // for audit log, use always the current security cache
             sc = getCache();
             logResource(sc, resourceId, instanceName, mode, found, "cached");
          }

          return found;
       }

       /**
        * Creates a search handle.
        * <p>
        * Plugins call this method to initiate the access rights search for
        * the specified resource instance and requested rights.
        * <p>
        * Note that the security cache has to be taken from the security context
        * of the thread.
        *
        * @param     resourceId
        *            The resource identity.
        * @param     instanceName
        *            The resource instance name.
        * @param     mode
        *            The access mode (requested rights) the decision was made about.
        *
        * @return    The integer handle value for the new search.
        */
       public int openRightsSearch(int resourceId, String instanceName, int mode)
       {
          if (!cfg.isServer())
             return 0;

          // Pick up the generation of the security cache
          SecurityContextStack ctx = SecurityContextStack.getContext();
          SecurityCache sc = ctx.getCache();
          int nres = sc.getNumResources();

          if (resourceId < 0 || resourceId >= nres)
          {
             return 0;
          }

          ResourceRegistry reg = sc.getRegistryByOrd(resourceId);
          Map searches = reg.getOpenSearches();
          AccessControlList[] acls = reg.getACLs();

          int[] checkList = ctx.getCheckList();

          // match ACL instance names
          AbstractResource plugin = reg.getPlugin();

          // create new open search
          Search s = new Search(instanceName,
                                mode,
                                checkList,
                                acls == null ?
                                   null :
                                   Arrays.stream(acls).filter(getAclMatcher(plugin, instanceName)));
          int handle = s.getHandle();

          // put it into the map of open searches
          synchronized (searches)
          {
             searches.put(handle, s);
          }

          // clear preexisting cached decision if any
          Decision d = ctx.getDecisionCache(resourceId);
          d.remove(instanceName, mode);

          return handle;

       }

       /**
        * Returns a predicate capable to filter access control list based on the supplied plugin and resource
        * name. The method calls {@link AbstractResource#getACLMatcher()} to retrieve a predicate, which is
        * then used to filter matching ACLs. If no predicate is returned by the supplied plugin a default
        * matching mechanism will be used as a fallback. The standard matching mechanism involves simple string
        * comparison of the supplied resource name and the resource name defined in the ACLs.
        *
        * @param   plugin
        *          A valid security plugin. Must not be {@code null}.
        * @param   instName
        *          Resource instance name.
        *
        * @return  An ACL predicate.
        */
       private static Predicate<AccessControlList> getAclMatcher(AbstractResource plugin, String instName)
       {
          BiPredicate<AccessControlList, String> aclMatcher = plugin.getACLMatcher();
          if (aclMatcher == null)
          {
             aclMatcher = (acl, name) -> acl.isExact() ?
                acl.getInstanceName().equalsIgnoreCase(name) :
                name.matches(acl.getInstanceName());
          }
          BiPredicate<AccessControlList, String> finalAclMatcher = aclMatcher;
          return (acl) -> finalAclMatcher.test(acl, instName);
       }

       /**
        * Returns an instance of Rights interface to be used next in the access
        * rights check loop.
        * <p>
        * Plugins cast Rights to their own classes and use their custom methods
        * to perform the check.
        * <p>
        * Note that the security cache has to be taken from the security context
        * of the thread.
        *
        * @param  handle
        *         search handle
        *
        * @return next available instance of <code>Rights</code>
        */
       @SuppressWarnings("rawtypes")
       public Rights getNextRights(int handle)
       {
          if (!cfg.isServer())
             return null;

          // Pick up the generation of the security cache
          SecurityContextStack ctx = SecurityContextStack.getContext();
          SecurityCache sc = ctx.getCache();

          int nres = sc.getNumResources();
          ResourceRegistry reg = null;
          Search s = null;
          Object key = handle; // autoboxing
          int i = 0;

          // find the search by its handle
          for (i = 0; i < nres; i++)
          {
             reg = sc.getRegistryByOrd(i);
             Map searches = reg.getOpenSearches();
             synchronized (searches)
             {
                if (searches.containsKey(key))
                {
                   s = (Search)searches.get(key);
                   break;
                }
             }
          }
          if (i == nres)
          {
             // handle not found
             return null;
          }

          // query the search for next Rights
          return s.next();
       }

       /**
        * Terminates the open search of Rights and frees up the related resources.
        * <p>
        * Note that the security cache has to be taken from the security context
        * of the thread.
        *
        * @param  handle
        *         search handle
        *
        * @param  decision
        *         <code>true</code> or <code>false</code> decision just made
        *
        * @param  cache
        *         <code>true</code> if the decision has to be cached
        */
       public void closeRightsSearch(int handle, boolean decision, boolean cache)
       {
          if (!cfg.isServer())
             return;

          // Pick up the generation of the security cache
          SecurityContextStack ctx = SecurityContextStack.getContext();
          SecurityCache sc = ctx.getCache();

          int nres = sc.getNumResources();
          ResourceRegistry reg = null;
          Search s = null;
          Object key = handle; //autoboxing
          int i = 0;

          // find the search by its handle
          for (i = 0; i < nres; i++)
          {
             reg = sc.getRegistryByOrd(i);
             Map searches = reg.getOpenSearches();
             synchronized (searches)
             {
                if (searches.containsKey(key))
                {
                   s = (Search) searches.get(key);
                   // dispose of the search
                   searches.remove(key);
                   break;
                }
             }
          }

          if (i == nres)
          {
             // handle not found
             return;
          }

          // for audit log, use always the current security cache
          sc = getCache();

          String name = s.getInstanceName();
          int    mode = s.getMode();

          // log the access decision event
          if (!cache)
          {
             logResource(sc, i, name, mode, decision);
             return;
          }
          else
          {
             logResource(sc, i, name, mode, decision, "cache");

             Decision d = ctx.getDecisionCache(i);
             d.cache(name, mode, decision);
          }
       }

       /**
        * Creates a search handle. This search handle is a temporary search,
        * created within the context of the currently authenticated subject. This
        * is used for cases when the current user needs to determine the actual
        * permissions for another subject, different then the authenticated one.
        * <p>
        * Plugins call this method to initiate the access rights search for
        * the specified resource instance and requested rights.
        * <p>
        * Note that the security cache has to be taken from the security context
        * of the thread.
        * <p>
        * The search handle is created for the specified subject.
        *
        * @param  subject
        *         subject ID to which this search belongs.
        *
        * @param  resourceId
        *         resource identity
        *
        * @param  instanceName
        *         resource instance name
        *
        * @param  mode
        *         access mode (requested rights) the decision was made about
        * @return integer handle value for the new search
        */
       @SuppressWarnings({ "rawtypes", "unchecked" })
       public int openRightsSearch(String subject,
                                   int resourceId,
                                   String instanceName,
                                   int mode)
       {
          if (!cfg.isServer() || subject == null)
             return 0;

          // Pick up the generation of the security cache
          SecurityContextStack ctx = SecurityContextStack.getContext();
          SecurityCache sc = ctx.getCache();
          int nres = sc.getNumResources();

          if (resourceId < 0 || resourceId >= nres)
          {
             return 0;
          }

          ResourceRegistry reg = sc.getRegistryByOrd(resourceId);
          Map searches = reg.getOpenSearches(subject, true);
          AccessControlList[] acls = reg.getACLs();

          // the checklist contains only the ordinal for the specified subject;
          // this means the permission search will be performed only among the
          // ACLs defined explicitly for this subject; if the subject is an user
          // which is part of some group, the group permissions will *NOT*
          // be searched
          int[] checkList = new int[] { getCache().getOrdinalById(subject) };

          // create new open search
          AbstractResource plugin = reg.getPlugin();
          Search s = new Search(instanceName,
                                mode,
                                checkList,
                                Arrays.stream(acls).filter(getAclMatcher(plugin, instanceName)));
          int handle = s.getHandle();

          synchronized (searches)
          {
             // put it into the map of open searches
             searches.put(handle, s); // autoboxing
          }

          // don't care about caching, just return the handle
          return handle;
       }

       /**
        * Returns an instance of Rights interface to be used next in the access
        * rights check loop. This search handle is a temporary search,
        * created within the context of the currently authenticated subject. This
        * is used for cases when the current user needs to determine the actual
        * permissions for another subject, different then the authenticated one.
        * <p>
        * Plugins cast Rights to their own classes and use their custom methods
        * to perform the check.
        * <p>
        * Note that the security cache has to be taken from the security context
        * of the thread.
        *
        * @param  subject
        *         subject ID to which the search handle belongs.
        *
        * @param  handle
        *         search handle
        *
        * @return next available instance of <code>Rights</code>
        */
       @SuppressWarnings("rawtypes")
       public Rights getNextRights(String subject, int handle)
       {
          if (!cfg.isServer() || subject == null)
             return null;

          // Pick up the generation of the security cache
          SecurityContextStack ctx = SecurityContextStack.getContext();
          SecurityCache sc = ctx.getCache();

          int nres = sc.getNumResources();
          ResourceRegistry reg = null;
          Search s = null;
          Object key = handle; //autoboxing
          int i = 0;

          // find the search by its handle
          for (i = 0; i < nres; i++)
          {
             reg = sc.getRegistryByOrd(i);
             Map searches = reg.getOpenSearches(subject, false);
             if (searches != null)
             {
                synchronized (searches)
                {
                   if (searches.containsKey(key))
                   {
                      s = (Search)searches.get(key);
                      break;
                   }
                }
             }
          }
          if (i == nres)
          {
             // handle not found
             return null;
          }

          // query the search for next Rights
          return s.next();
       }

       /**
        * Terminates the open search of Rights and frees up the related resources.
        * <p>
        * Note that the security cache has to be taken from the security context
        * of the thread.
        *
        * @param  subject
        *         subject ID to which the search handle belongs.
        *
        * @param  handle
        *         search handle
        *
        * @param  decision
        *         <code>true</code> or <code>false</code> decision just made
        */
       @SuppressWarnings("rawtypes")
       public void closeRightsSearch(String subject, int handle, boolean decision)
       {
          if (!cfg.isServer() || subject == null)
             return;

          // Pick up the generation of the security cache
          SecurityContextStack ctx = SecurityContextStack.getContext();
          SecurityCache sc = ctx.getCache();

          int nres = sc.getNumResources();
          ResourceRegistry reg = null;
          Search s = null;
          Object key = handle; //autoboxing
          int i = 0;

          // find the search by its handle
          for (i = 0; i < nres; i++)
          {
             reg = sc.getRegistryByOrd(i);
             Map searches = reg.getOpenSearches(subject, false);
             if (searches != null)
             {
                synchronized (searches)
                {
                   if (searches.containsKey(key))
                   {
                      // dispose of the search
                      s = (Search)searches.get(key);
                      searches.remove(key);

                      if (searches.isEmpty())
                      {
                         reg.removeOpenSearches(subject);
                      }

                      break;
                   }
                }
             }
          }
          if (i == nres)
          {
             // handle not found
             return;
          }

          logResource(sc, i, s.getInstanceName(), s.getMode(), decision);
       }

       /**
        * Compiles and evalautes an expression.
        * <p>
        * The expression may refer to Security Manager's variables and functions
        * and to the plugin's variables and functions, should one decide to expose
        * any.
        * <p>
        * The current implementation arbitrates names clashes using natural
        * scoping provided by Expression Engine, which can be modified by using
        * qualifiers in the expression.
        * <p>
        * Note that the security cache has to be taken from the security context
        * of the thread.
        *
        * @param  resourceId
        *         resource identity
        *
        * @param  link
        *         an Object that supplies the raw data for this computation and is
        *         shared between the resource plugin and its library of exports
        *
        * @param  expr
        *         expression to compile and evaluate
        *
        * @return the result. If expression compilation fails,
        *         <code>null</code> is returned
        */
       public synchronized Object evaluate(int resourceId, Object link,
                                           String expr)
       {
          if (!cfg.isServer())
             return null;

          // Pick up the generation of the security cache
          SecurityContextStack ctx = SecurityContextStack.getContext();
          SecurityCache sc = ctx.getCache();
          int nres = sc.getNumResources();

          if (resourceId < 0 || resourceId >= nres)
          {
             return null;
          }

          // linking the resource plugin and rights with the expression
          // TODO: the current generation of the security cache used here may be different than the
          // one used when resolving the expression; need to explicitly pass the security cache
          // generation saved in "sc" to the resolver
          AbstractResource ar = sc.getRegistryByOrd(resourceId).getPlugin();
          Resolver resolver = sc.getResolver();

          ar.associate(link);
          resolver.setCurrentScope(ar);

          // compilation and evaluation
          Object result = null;
          try
          {
             Expression express = new Expression(resolver, expr);
             result = express.execute();
          }
          catch (Exception exc)
          {
             LOG.warning("", exc);
             result = null;
          }

          ar.disassociate();

          return result;
       }

       /**
        * Notifies the Security Manager about a batch editing session to be
        * started for the specified directory branch.
        * If the branch is either /security or any of its branches, notes the
        * state of batch editing.
        * <p>
        * <i><u>Restricted use</u></i>. This method checks the caller to be:
        * <ul>
        *  <li> method openBatch in com.goldencode.p2j.directory.DirectoryService
        * </ul>
        * <p>
        * DirectoryService calls this method before it opens a batch. This method
        * checks the subject's access rights to the "system", "change" instance,
        * if the batch is for any branch of "/security".
        * <p>
        * The action is different for master servers versus regular ones.
        * Only master servers are allowed to edit the P2J security directory.
        * Regular servers cannot edit, but still need this call as a way of
        * refreshing their internal caches.
        * <p>
        * Note that the security cache has to be taken from the security context
        * of the thread.
        *
        * @param nodeId
        *           Subtree which will be edited.
        * @return One of the following codes:
        *         <ul>
        *           <li>-1 if the subject has no rights to open batch or another
        *                  batch is currently open
        *           <li> 0 if the batch can be opened
        *           <li> 1 if the batch can be opened R/O; not a master server
        *         </ul>
        */
       public int openBatch(String nodeId)
       {
          // optimally, this would be done after the checkCaller below, but that
          // call is very expensive and by putting this here we allow the call
          // to this method but the caller gets no service so security is not
          // really reduced
          if (!cfg.isServer())
             return -1;

          // restrict the use of this method
          if (!SecurityUtil.checkCaller(
              "com.goldencode.p2j.directory.DirectoryService.openBatch"))
             return -1;

          // accessing security context
          if (!init)
          {
             return 0;
          }

          SecurityContextStack ctx = SecurityContextStack.getContext();
          if (ctx.isEditing())
             return -1;

          // check to see if the batch is under /security
          if (!nodeId.equals("/security") && !nodeId.startsWith("/security/"))
             return 0;

          // fail the call if not sufficient access rights (if appl context)
          SecurityContext sctx = ctx.getEffectiveContext();
          if (!sctx.isInitial())
          {
             SystemResource sr = (SystemResource)getPluginInstance("system");
             if (!sr.checkChangeAccess())
                return -1;
          }

          // note editing batch in the context
          ctx.setEditing(true);

          // Pick up the generation of the security cache
          SecurityCache sc = ctx.getCache();

          // do not open R/W security editing batch for non-masters
          if (!sc.getServerAccount().isMaster())
             return 1;
          else
             return 0;
       }

       /**
        * Checks to see if a batch editing is in progress.
        * <p>
        * <i><u>Restricted use</u></i>. This method checks the caller to be:
        * <ul>
        *  <li> method isEditing in com.goldencode.p2j.directory.DirectoryService
        * </ul>
        *
        * @return <code>true</code> if there is an open editing batch
        */
       public boolean isEditing()
       {
          // optimally, this would be done after the checkCaller below, but that
          // call is very expensive and by putting this here we allow the call
          // to this method but the caller gets no service so security is not
          // really reduced
          if (!cfg.isServer())
             return false;

          // restrict the use of this method
          if (!SecurityUtil.checkCaller(
              "com.goldencode.p2j.directory.DirectoryService.isEditing"))
             return false;

          // accessing security context
          return SecurityContextStack.getContext().isEditing();
       }

       /**
        * Notifies the Security Manager about a batch editing session to be
        * closed assuming the security cache to be refreshed.
        * <p>
        * DirectoryService calls this method after having successfully commited
        * the changes to the directory (disposition is <code>true</code>) or to
        * signal rollback.
        * <p>
        * <i><u>Restricted use</u></i>. This method checks the caller to be:
        * <ul>
        *  <li> method closeBatchInt in
        *       com.goldencode.p2j.directory.DirectoryService
        * </ul>
        *
        * @param    disposition
        *           If this parameter is <code>true</code> then changes need to
        *           be committed. Otherwise they are just thrown away and lock is
        *           removed.
        * @return   <code>true</code> if operation was successful and tree was
        *           successfully updated.
        */
       public boolean closeBatch(boolean disposition)
       {
          return closeBatch(disposition, true);
       }

       /**
        * Notifies the Security Manager about a batch editing session to be
        * closed.
        * <p>
        * DirectoryService calls this method after having successfully commited
        * the changes to the directory (disposition is <code>true</code>) or to
        * signal rollback.
        * <p>
        * <i><u>Restricted use</u></i>. This method checks the caller to be:
        * <ul>
        *  <li> method closeBatchInt in
        *       com.goldencode.p2j.directory.DirectoryService
        * </ul>
        *
        * @param    disposition
        *           If this parameter is <code>true</code> then changes need to
        *           be committed. Otherwise they are just thrown away and lock is
        *           removed.
        * @param    refresh
        *           when committing the batch, tells whether to refresh the
        *           security cache or not
        * @return   <code>true</code> if operation was successful and tree was
        *           successfully updated.
        */
       public boolean closeBatch(boolean disposition, boolean refresh)
       {
          // optimally, this would be done after the checkCaller below, but that
          // call is very expensive and by putting this here we allow the call
          // to this method but the caller gets no service so security is not
          // really reduced
          if (!cfg.isServer())
          {
             LOG.warning("Batch cannot be closed from non-server context.");
             return false;
          }

          // restrict the use of this method
          if (!SecurityUtil.checkCaller(
              "com.goldencode.p2j.directory.DirectoryService.closeBatchInt"))
             return false;

          // accessing security context
          if (!init)
          {
             return true;
          }

          SecurityContextStack ctx = SecurityContextStack.getContext();
          if (!ctx.isEditing())
          {
             LOG.warning("Batch cannot be closed, it is not open for edit in the current " +
                         "security context.");
             return false;
          }

          ctx.setEditing(false);

          // refreshing the security cache: no refresh if rollback or no request
          if (!disposition || !refresh)
             return true;

          // refreshing the security cache: no refresh if by a worker thread
          SecurityContext sctx = ctx.getEffectiveContext();
          if (sctx.isInitial())
             return true;

          // refreshing the security cache
          SecurityCacheRefresh ref = new SecurityCacheRefresh(SecurityCache.
                                                              LM_CONSOLE, null);
          Thread thread;
          thread = new Thread(secThreads, ref, "refresh");

          thread.start();

          return true;
       }

       /**
        * Checks access rights of the current subject with regards to the
        * "shutdown" instance of the "system" abstract resource.
        *
        * @return <code>true</code> if access is allowed.
        */
       public boolean checkShutdownAccess()
       {
          if (!cfg.isServer())
             return false;

          SystemResource sr = (SystemResource)getPluginInstance("system");
          return sr.checkShutdownAccess();
       }

       /**
        * Checks if the current context is the server one.
        * <p>
        * Note that the value returned by this method is sometimes different that {@link #isServer()}.
        * This is because {@code isServer()} will return {@code true} on all contexts created on
        * P2J server application, while this method will return {@code false} for eventual client
        * contexts additionally created inside the process (like an appserver <code>Agent</code>
        * class instance entity).
        *
        * @return  {@code true} only when called on server account context.
        */
       public boolean isServerAccount()
       {
          if (isClient())
          {
             return false; // server context cannot exist on client-side
          }

          // Pick up the generation of security cache
          SecurityContextStack ctxs = SecurityContextStack.getContext();
          SecurityContext ctx = ctxs.getEffectiveContext();
          SecurityCache sc = ctx.getCache();

          // figure out subject ordinal (must be process)
          int userOrd = ctx.getProcessOrdinal();
          if (userOrd != -1)
          {
             ProcessAccount pc = (ProcessAccount) sc.getAccountByOrd(userOrd);
             return pc.isServer();
          }
          return false;
       }

       /**
        * Returns {@code true} if this is the server application.
        * <p>
        * Ultimately, this method checks the {@code type} attribute of the root node of the bootstrap
        * configuration file. If the current process was launched using a {@code server} value, then
        * this method returns {@code true}.
        *
        * @return  {@code true} if current process is the P2J server application.
        */
       public boolean isServer()
       {
          return cfg.isServer();
       }

       /**
        * Returns {@code true} if this is a client (not server) application. This is the complementary
        * of the above method.
        *
        * @return  {@code true} if on the client.
        */
       public boolean isClient()
       {
          return !isServer();
       }

       /**
        * Checks whether the specified user account exists.
        *
        * @param  uid
        *         user ID
        *
        * @return <code>true</code> if such user account exists
        */
       public boolean isUserDefined(String uid)
       {
          Account acc = getAccount(uid);

          // check the account type
          return (acc != null && acc.getAccountType() == Account.ACC_USER);
       }

       /**
        * Checks whether the specified process account exists.
        *
        * @param  pid
        *         process ID
        *
        * @return <code>true</code> if such process account exists
        */
       public boolean isProcessDefined(String pid)
       {
          Account acc = getAccount(pid);

          // check the account type
          return (acc != null && acc.getAccountType() == Account.ACC_PROCESS);
       }

       /**
        * Resolve the appserver associated with the given process ID.
        *
        * @param    pid
        *           The process ID.
        *
        * @return   The appserver or {@code null} if the passed ID is not for a process or there is no
        *           associated appserver for the process.
        */
       public String getAppserverForProcess(String pid)
       {
          Account acc = getAccount(pid);

          // check the account type
          return (acc != null && acc.getAccountType() == Account.ACC_PROCESS)
                    ? ((ProcessAccount) acc).getAppServer()
                    : null;
       }

       /**
        * Resolve the broker associated with the given process ID.
        *
        * @param    pid
        *           The process ID.
        *
        * @return   The broker or {@code null} if the passed ID is not for a process or there is no
        *           associated broker for the process.
        */
       public String getBrokerForProcess(String pid)
       {
          Account acc = getAccount(pid);

          // check the account type
          return (acc != null && acc.getAccountType() == Account.ACC_PROCESS)
                    ? ((ProcessAccount) acc).getBroker()
                    : null;
       }

       /**
        * Checks whether the specified group account exists.
        *
        * @param  uid
        *         group ID
        *
        * @return <code>true</code> is such group account exists
        */
       public boolean isGroupDefined(String uid)
       {
          Account acc = getAccount(uid);

          // check the account type
          return (acc != null && acc.getAccountType() == Account.ACC_GROUP);
       }

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

       /**
        * Gets IDs of all defined user accounts.
        *
        * @return array of all defined user IDs
        */
       public String[] getAllUsers()
       {
          // check the access rights
          SystemResource sr = (SystemResource)getPluginInstance("system");
          boolean decision = sr.checkAccountsAccess();
          if (!decision)
             return new String[] {};

          // Pick up the generation of security cache
          SecurityContextStack ctxs = SecurityContextStack.getContext();
          SecurityContext ctx = ctxs.getEffectiveContext();
          SecurityCache sc = ctx.getCache();

          // get and return tha array
          return sc.getAllAccountIds(Account.ACC_USER);
       }

       /**
        * Gets IDs of all defined accounts.
        *
        * @return array of all defined subject IDs
        */
       public String[] getAllSubjects()
       {
          // check the access rights
          SystemResource sr = (SystemResource)getPluginInstance("system");
          boolean decision = sr.checkAccountsAccess();
          if (!decision)
             return new String[] {};

          // Pick up the generation of security cache
          SecurityContextStack ctxs = SecurityContextStack.getContext();
          SecurityContext ctx = ctxs.getEffectiveContext();
          SecurityCache sc = ctx.getCache();

          // get and return tha array
          return sc.getAllAccountIds(Account.ACC_ALL);
       }

       /**
        * Gets IDs by thier ordinals.
        *
        * @param ordinal
        *        ordinal number of some subject ID
        * @return a subject ID that corresponds to the ordinal number given.
        */
       public String getIdByOrdinal(int ordinal)
       {
          // check the access rights
          SystemResource sr = (SystemResource)getPluginInstance("system");
          boolean decision = sr.checkAccountsAccess();
          if (!decision)
             return null;

          // Pick up the generation of security cache
          SecurityContextStack ctxs = SecurityContextStack.getContext();
          SecurityContext ctx = ctxs.getEffectiveContext();
          SecurityCache sc = ctx.getCache();

          // get the account
          Account subj = sc.getAccountByOrd(ordinal);
          if (subj == null)
             return null;

          return subj.getSubjectId();
       }

       /**
        * Returns the array of all ACLs defined for the specified resource type.
        * <p>
        *
        * @param  resourceId
        *         resource identity
        * @return array of <code>AccessControlList</code>s. The array is empty if
        *         the caller has no access rights to the system/accounts resource
        *         instance.
        */
       public AccessControlList[] getACLs(int resourceId)
       {
          // check the access rights
          SystemResource sr = (SystemResource)getPluginInstance("system");
          boolean decision = sr.checkAccountsAccess();
          if (!decision)
             return new AccessControlList[] {};

          // Pick up the generation of the security cache
          SecurityContextStack ctx = SecurityContextStack.getContext();
          SecurityCache sc = ctx.getCache();
          int nres = sc.getNumResources();

          if (resourceId < 0 || resourceId >= nres)
          {
             return new AccessControlList[] {};
          }

          // access the plugins registry and the ACLs
          ResourceRegistry reg = sc.getRegistryByOrd(resourceId);
          AccessControlList[] acls = reg.getACLs();

          // making an array copy to give away
          AccessControlList[] ret = new AccessControlList[acls.length];
          for (int i = 0; i < acls.length; i++)
          {
             ret[i] = acls[i];
          }

          return ret;
       }

       /**
        * Get a map of consolidated ACL's, per each function. For each function
        * name there will be a map of subject indexes to their permissions.
        *
        * @param   type
        *          The resource type name for which the consolidated ACLs are
        *          needed
        *
        * @return  a map of consolidated ACL's, for the specified resource type.
        */
       public Map<String, Map<Integer, Rights>> getConsolidatedACLs(String type)
       {
          AbstractResource resource = getPluginInstance(type);

          if (resource == null)
          {
             return Collections.emptyMap();
          }

          Map<String, Map<Integer, Rights>> result =
             new HashMap<String, Map<Integer, Rights>>();

          // for each instance name, create a set of subjects with explicit
          // permissions for that instance name
          Map<String, Set<Integer>> subjectsByInstances =
             getSubjectsByInstanceName(resource.resourceIndex);

          SecurityCache sc = getCache();

          Iterator<String> iter = subjectsByInstances.keySet().iterator();
          while (iter.hasNext())
          {
             String instanceName = iter.next();
             Set<Integer> subjects = subjectsByInstances.get(instanceName);

             Map<Integer, Rights> permissions = result.get(instanceName);

             if (permissions == null)
             {
                permissions = new HashMap<Integer, Rights>();
                result.put(instanceName, permissions);
             }

             // iterate the known subjects with explicit permissions and compute
             // the "consolidated" permissions
             for (Integer subjectIdx : subjects)
             {
                Account acc = sc.getAccountByOrd(subjectIdx);
                if (acc == null)
                {
                   continue; // not a valid account
                }

                Rights rights = resource.getPermissions(acc.getSubjectId(),
                                                        instanceName);

                if (rights != null)
                {
                   permissions.put(subjectIdx, rights);
                }
             }
          }

          return result;
       }

       /**
        * Consolidate all the subjects for each instance with defined ACLs, for
        * the given resources.
        *
        * @param   resourceId
        *          resource identity
        *
        * @return  a map which holds a set of subjects for each resource instance
        */
       private Map<String, Set<Integer>> getSubjectsByInstanceName(int resourceId)
       {
          // check the access rights
          SystemResource sr = (SystemResource)getPluginInstance("system");
          boolean decision = sr.checkAccountsAccess();
          if (!decision)
             return Collections.emptyMap();

          // Pick up the generation of the security cache
          SecurityContextStack ctx = SecurityContextStack.getContext();
          SecurityCache sc = ctx.getCache();
          int nres = sc.getNumResources();

          if (resourceId < 0 || resourceId >= nres)
          {
             return Collections.emptyMap();
          }

          // access the plugins registry and the ACLs
          ResourceRegistry reg = sc.getRegistryByOrd(resourceId);
          AccessControlList[] acls = reg.getACLs();

          Map<String, Set<Integer>> result = new HashMap<String, Set<Integer>>();

          for (int i = 0; i < acls.length; i++)
          {
             String instanceName = acls[i].getInstanceName();

             Set<Integer> subjects = result.get(instanceName);
             if (subjects == null)
             {
                subjects = new HashSet<Integer>();
                result.put(instanceName, subjects);
             }

             // get all bindings for this resource instance
             List<Binding> binds = acls[i].getBindings();

             // collect the subjects
             Iterator<Binding> iter = binds.iterator();
             while (iter.hasNext())
             {
                Binding bind = iter.next();

                int[] boundedSubjects = bind.getSubjects();
                subjects.addAll(Utils.primitiveToIntegerList(boundedSubjects));
             }
          }

          return result;
       }

       /**
        * Initiates the password change procedure, calls the configured password
        * input hook, and changes the password for the current user when it is
        * verified.
        *
        * @return  <code>true</code> if password has been changed successfully.
        */
       public boolean changePassword()
       {
          // Pick up the generation of the security cache
          SecurityCache sc = getCache();

          // get the password change hook
          String pwiClassName = sc.getPasswordInput();
          PasswordInput pwi = null;

          // create an instance of the class
          try
          {
             Class<?> pwiClass = Class.forName(pwiClassName);
             pwi = (PasswordInput)pwiClass.getDeclaredConstructor().newInstance();
          }
          catch (Exception e)
          {
             return false;
          }

          if (pwi == null)
             return false;

          // get the current password's digest
          String uid = getUserId();
          Account acc = sc.getAccountById(uid);
          if (acc == null)
             return false;
          if (acc.getAccountType() != Account.ACC_USER)
             return false;

          UserAccount user = (UserAccount)acc;

          byte[] hashpw = null;

          synchronized (pwchSync)
          {
             hashpw = user.getPassword();
          }

          // run the password input hook
          String newpw = pwi.obtainPassword(hashpw);
          if (newpw == null)
             return false;

          // set new password
          return setPassword(newpw);
       }

       /**
        * Gets password age status for the current account.
        *
        * @return <code>true</code> if this password is too old and needs changing
        */
       public boolean isPasswordAged()
       {
          // Pick up the generation of the security cache
          SecurityCache sc = getCache();

          // get the current account
          String uid = getUserId();
          Account acc = sc.getAccountById(uid);
          if (acc == null)
             return false;
          if (acc.getAccountType() != Account.ACC_USER)
             return false;

          UserAccount user = (UserAccount)acc;
          return user.isPasswordAged();
       }

       /**
        * Returns the string representation of this object.
        *
        * @return string describing this object
        */
       @Override
       public String toString()
       {
          return "Security Manager";
       }

       /**
        * Safely gets the reference to the security cache in a multithreaded
        * environment.
        * @return reference to the curent copy of <code>SecurityCache</code>
        */
       synchronized SecurityCache getCache()
       {
          return cache;
       }

       /**
        * Returns the bootstrap configuration used to construct this instance.
        *
        * @return instance of <code>BootstrapConfig</code> associated with this
        *         object
        */
       BootstrapConfig getConfig()
       {
          return cfg;
       }

       /**
        * Checks the rights of the caller to perform admin operations.
        *
        * @return <code>true</code> if the caller has sufficient rights
        */
       private boolean adminAccess()
       {
          // check the access rights with the admin plugin first
          AdminResource ar = (AdminResource)getPluginInstance("admin");
          if (ar != null)
          {
             return ar.checkAnyAdminAccess();
          }

          // fall back to the system resource
          SystemResource sr = (SystemResource)getPluginInstance("system");
          return sr.checkAdminAccess();
       }

       /**
        * Get the account associated with the given user id.
        *
        * @param  uid
        *         user ID
        *
        * @return The account, if it exists, and the caller has sufficient rights
        *         to retrieve it, else <code>null</code>
        */
       private Account getAccount(String uid)
       {
          // check the access rights
          SystemResource sr = (SystemResource)getPluginInstance("system");
          boolean decision = sr.checkAccountsAccess();
          if (!decision)
             return null;

          // Pick up the generation of security cache
          SecurityContextStack ctxs = SecurityContextStack.getContext();
          SecurityContext ctx = ctxs.getEffectiveContext();
          SecurityCache sc = ctx.getCache();

          // find the account
          Account acc = sc.getAccountById(uid);

          return acc;
       }

       /**
        * Creates an audit record for a resource instance access event.
        *
        * @param  cache
        *         security cache where to log this event
        *
        * @param  resourceId
        *         resource identity
        *
        * @param  instanceName
        *         resource instance name
        *
        * @param  mode
        *         access mode (requested rights) the decision was made about
        *
        * @param  decision
        *         access decision
        *
        * @param  message
        *         additional text
        */
       void logResource(SecurityCache cache, int resourceId, String instanceName,
                        int mode, boolean decision, String message)
       {
          Audit audit = cache.getAudit();
          audit.log(resourceId, instanceName, mode, decision, message);
       }

       /**
        * Creates an audit record for a resource instance access event.
        *
        * @param  cache
        *         security cache where to log this event
        *
        * @param  resourceId
        *         resource identity
        *
        * @param  instanceName
        *         resource instance name
        *
        * @param  mode
        *         access mode (requested rights) the decision was made about
        *
        * @param  decision
        *         access decision
        */
       void logResource(SecurityCache cache, int resourceId, String instanceName,
                        int mode, boolean decision)
       {
          Audit audit = cache.getAudit();
          audit.log(resourceId, instanceName, mode, decision);
       }

       /**
        * Sets new password for the current user account.
        *
        * @param   newPassword
        *          new password as plain text
        * @return  <code>true</code> if password has been set successfully
        */
       boolean setPassword(String newPassword)
       {
          // find the security cache
          SecurityContextStack ctxs = SecurityContextStack.getContext();
          SecurityContext      ctx  = ctxs.getEffectiveContext();

          // figure out 1st subject ordinal
          int ord = ctx.getUserOrdinal();

          if (ord == -1)
          {
             // we aren't a user! sorry, no password change for processes
             return false;
          }

          PasswordChange pwch = new PasswordChange(ord, newPassword);
          Thread thread;
          thread = new Thread(secThreads, pwch, "passwordchange");

          synchronized (pwchSync)
          {
             thread.start();
             try
             {
                thread.join();
             }
             catch (InterruptedException ix)
             {
                return false;
             }
          }

          return pwch.result;
       }

       /**
        * Send the client's requested authorization type on the given connection.
        * <p>
        * This method inspects the bootstrap configuration to see what kind of
        * authentication to pick:<ul>
        *   <li> <code>security.keystore.processalias</code> is set;
        *        <code>AUTH_REQ_PROCESS</code> will be sent.
        *   <li> <code>security.keystore.processalias</code> is not set, but
        *        <code>security.authentication.type</code> is set and equals
        *        <code>PROGRAM</code>;
        *        <code>AUTH_REQ_PROGRAM</code> will be sent.
        *   <li> otherwise
        *        <code>AUTH_REQ_USER</code> will be sent.
        * </ul>
        *
        * @param    socket
        *           The socket on which to write our type.
        * @param    cfg
        *           The attempted connection's configuration.
        *
        * @return   <code>true</code> if successful
        */
       private boolean sendAuthType(NetSocket socket, BootstrapConfig cfg)
       {
          // authentication type to be determined dynamically
          int authType = -1;

          try
          {
             String alias = cfg.getConfigItem("security", "keystore", "processalias");
             if (alias != null)
             {
                // use process type authentication
                authType = AUTH_REQ_PROCESS;
             }
          }
          catch (ConfigurationException ignored)
          {
          }

          // choose between interactive and programmatic types if not process
          if (authType == -1)
          {
             // check to see if security.authentication.type is set
             try
             {
                String type = cfg.getConfigItem("security", "authentication", "type");

                if (type != null && type.equalsIgnoreCase("program"))
                {
                   // use programmatic type authentication
                   authType = AUTH_REQ_PROGRAM;
                }
             }
             catch (ConfigurationException ignored)
             {
             }
          }

          if (authType == -1)
          {
             authType = AUTH_REQ_USER;
          }

          if (LOG.isLoggable(Level.FINER))
          {
             LOG.finer("Sending AUTHTYPE: " + authType);
          }
          try
          {
             // send the packet
             socket.writeInt(authType);
             if (authType == AUTH_REQ_USER)
             {
                String userName = "";
                try
                {
                   userName = cfg.getConfigItem("access", "subject", "id");
                   if (userName == null)
                      userName = "";
                }
                catch (ConfigurationException e)
                {
                   LOG.warning("Error accessing subject id: ", e);
                }

                socket.writeUTF(userName);
             }

             socket.flush();
          }

          catch (IOException ioe)
          {
             return false;
          }

          if (LOG.isLoggable(Level.FINER))
          {
             LOG.finer("Sent AUTHTYPE");
          }

          return true;
       }

       /**
        * Safely sets the reference to the security cache in a mutithreaded
        * environment.
        * <p>
        * In the multithreaded environment, after the closing of a batch editing
        * session, a newly created thread rereads the directory and builds a new
        * instance of <code>SecurityCache</code> while the existing one continues
        * to serve the needs of the server. This call is the last step in the
        * refreshing of the security cache.
        *
        * @param  cache
        *         reference to a copy of <code>SecurityCache</code>
        */
       private synchronized void setCache(SecurityCache cache)
       {
          this.cache = cache;
       }

       /**
        * Instantiates and runs the server side of the custom authentication hook.
        * <p>
        * Accepts the byte array produced by the client side authorization hook
        * as the authentication input, and custom parameters.
        *
        * @param    sc
        *           The security cache generation to be used.
        * @param    targetHookName
        *           The name of the custom auth hook to run as it is represented
        *           into the directory. Cannot be <code>null</code>.
        * @param    serverAuth
        *           The instance of the server hook or <code>null</code> to
        *           obtain the default processing.
        * @param    entity
        *           Custom parameters.
        * @param    auth
        *           The authorization input from the client.
        *
        * @return   The result of the authentication processing.
        */
       private AuthenticationResponse runCustomServerHook(SecurityCache sc,
                                                          String        targetHookName,
                                                          Authenticator serverAuth,
                                                          String        entity,
                                                          byte[]        auth)
       {
          AuthenticationResponse result = null;

          if (serverAuth == null)
          {
             // no user-specified replacement, so use the default approach
             serverAuth = serverAuthenticator;
          }

          result = serverAuth.serverAuthHook(auth, entity);
          int resCode = result.getReturnCode();
          if (resCode != AUTH_RESULT_SUCCESS)
          {
             return result;
          }

          String userId = result.getSubject();
          LOG.finer("Received user ID <" + userId + ">");

          // locate the account
          Account acc = sc.getAccountById(userId);
          if (acc == null)
          {
             LOG.finer("No account for ID <" + userId + ">");
             return new AuthenticationResponse(null, AUTH_RESULT_INVALID_USERID);
          }

          // TODO: Why is this restriction needed? By doing this, one cannot
          //       use the custom auth method to authenticate processes, right?
          // (before changing this check TrustedClientPlugin behavior)
          if (acc.getAccountType() != Account.ACC_USER)
          {
             LOG.finer("Wrong account type for ID <" + userId + ">");
             return new AuthenticationResponse(null, AUTH_RESULT_UNSPECIFIED_FAILURE);
          }

          UserAccount userAccount = (UserAccount) acc;
          if (userAccount.getAuthMode() != AUTH_MODE_CUSTOM)
          {
             LOG.finer("Account <" + userId + "> uses " + userAccount.getAuthMode() +
                    " auth mode, cancelling custom auth");
             return new AuthenticationResponse(null, AUTH_RESULT_INVALID_USERID);
          }

          if (userAccount.getAuthPlugin() == null ||
              !userAccount.getAuthPlugin().equals(targetHookName))
          {
             LOG.finer("Account <" + userId + "> uses " + userAccount.getAuthPlugin() +
                    " auth plugin, cancelling custom auth");
             return new AuthenticationResponse(null, AUTH_RESULT_INVALID_USERID);
          }

          return result;
       }

       /**
        * Filter allowed ciphers
        * @param ciphers
        *        available ciphers
        * @return filtered cipers.
        */
       public String[] filterCiphers(String[] ciphers)
       {
          return useGCM ? ciphers :
             Arrays.stream(ciphers).
             filter(s -> s.indexOf("_GCM_") < 0).
             collect(Collectors.toList()).toArray(new String[0]);
       }

       /**
        * Worker to send back the authentication result and a disposition code.
        *
        * @param    socket
        *           The socket to send on.
        * @param    authResult
        *           The authentication result.
        * @param    authAction
        *           The authentication disposition code.
        *
        * @return   <code>true</code> if sent successfully.
        */
       private boolean sendAuthResult(NetSocket socket, int authResult, int authAction)
       {
          boolean isFinerLoggable = LOG.isLoggable(Level.FINER);
          if (isFinerLoggable)
          {
             LOG.finer("Sending AUTHRESULT" + authResult + " (action = " + authAction + ")");
          }

          try
          {
             // send the packet
             socket.writeInt(authResult);
             socket.writeInt(authAction);
             socket.flush();
          }
          catch (IOException ioe)
          {
             LOG.severe("Error sending AUTHRESULT " + ioe);
             return false;
          }

          if (isFinerLoggable)
          {
             LOG.finer("Sent AUTHRESULT");
          }

          return true;
       }

       /**
        * Get the default "option" value (specified into directory) for the given
        * custom auth hook.
        *
        * @param   cache
        *          The security cache generation to be used.
        * @param   hookName
        *          The name of the target custom hook.
        *
        * @return  the "option" value for the given custom auth hook.
        */
       private String getHookParam(SecurityCache cache, String hookName)
       {
          AuthPlugin plugin = cache.getAuthPlugin(hookName);
          return plugin != null ? plugin.getOption() : null;
       }

       /**
        * Check if the given account is allowed to be used by an authentication request.
        *
        * @param   pid
        *          The local process account being authenticated.
        * @param   uid
        *          The local user account being authenticated.
        * @param   iact
        *          {@code true} if this is a call that can be an interactive user session.
        *
        * @return  If the account is allowed to be used, it will return <code>null</code>.
        *          Otherwise, it returns an Object[5] array with the indexes:
        *          0 = auth result (see SecurityConstants.AUTH_RESULT*),
        *          1 = auth action (see SecurityConstants.AUTH_ACTION*),
        *          2 = not used
        *          3 = not used
        *          4 = not used
        */
       Object[] allowAccount(String pid, String uid, boolean iact)
       {
          SecurityCache sc = getCache();
          Object[] res = new Object[5];
          boolean allow = true;

          if (pid != null)
          {
             ProcessAccount pa = (ProcessAccount) sc.getAccountById(pid);
             if (pa == null)
             {
                allow = false;
             }
             else
             {
                allow = pa.isEnabled();
             }
          }

          if (allow && uid != null)
          {
             UserAccount ua = (UserAccount) sc.getAccountById(uid);
             if (ua == null)
             {
                allow = false;
             }
             else
             {
                allow = ua.isEnabled();

                // we only check user limits for interactive users
                if (allow && iact)
                {
                   if (!checkUserAccountLimit(sc, uid))
                   {
                      res[0] = AUTH_RESULT_PER_USER_SESSION_LIMIT_REACHED;
                      res[1] = AUTH_ACTION_ABORT;
                      return res;
                   }

                   if (!checkMaxSystemUsers(sc, uid))
                   {
                      res[0] = AUTH_RESULT_SYSTEM_SESSION_LIMIT_REACHED;
                      res[1] = AUTH_ACTION_ABORT;
                      return res;
                   }
                }
             }
          }

          if (!allow)
          {
             LOG.finer("Account disabled");

             res[0] = AUTH_RESULT_INSUFFICIENT_RIGHTS;
             res[1] = AUTH_ACTION_ABORT;
             return res;
          }

          return null;
       }

       /**
        * Implements the server side of the authentication procedure for
        * processes or interactive clients. The method validates the process and
        * user accounts, creates new security context, creates new session and
        * check access rights to the system/logon resource.
        *
        * @param   sid
        *          The new session id.
        * @param   pid
        *          The local process account being authenticated.
        * @param   uid
        *          The local user account being authenticated.
        * @param   iact
        *          {@code true} if this is a call that can be an interactive user session.
        *
        * @return  an Object[5] array with the indexes:
        *          0 = auth result (see SecurityConstants.AUTH_RESULT*),
        *          1 = auth action (see SecurityConstants.AUTH_ACTION*),
        *          2 = app server name associated with the supplied process account,
        *          3 = the created security context,
        *          4 = the created session.
        *
        * @throws  RestrictedUseException
        */
       Object[] postAuthenticateWorker(Object sid, String pid, String uid, boolean iact)
       throws RestrictedUseException
       {
          Object[] res = allowAccount(pid, uid, iact);
          if (res != null)
          {
             return res;
          }

          SecurityCache sc = getCache();
          res = new Object[5];
          int authResult;
          int authAction;
          String appServer = null;

          if (pid != null)
          {
             ProcessAccount pa = (ProcessAccount) sc.getAccountById(pid);
             appServer = pa.getAppServer();
          }

          authResult = AUTH_RESULT_SUCCESS;
          authAction = AUTH_ACTION_DONE;

          // create new security context
          SecurityContext newContext = contextSm.createSecurityContext(sc, pid, uid);

          if (newContext == null || sc != getCache())
          {
             LOG.finer("authenticateLocal: newer security cache generation available; " +
                    "authentication aborted.");

             authResult = AUTH_RESULT_UNSPECIFIED_FAILURE;
             authAction = AUTH_ACTION_ABORT;
             res[0] = authResult;
             res[1] = authAction;
             return res;
          }

          // create new session
          SecuritySession sess = sessionSm.addSession(sid, uid, sc, newContext);

          contextSm.assignContext(newContext);

          // check access rights to the system/logon resource
          contextSm.pushContextWorker(newContext);
          SystemResource sr = (SystemResource) getPluginInstance("system");
          boolean decision = sr.checkLogonAccess();
          contextSm.popContextWorker();

          if (!decision)
          {
             LOG.finer("Access denied to system/logon resource");
             authResult = AUTH_RESULT_INSUFFICIENT_RIGHTS;
             authAction = AUTH_ACTION_ABORT;
             appServer = null;
             newContext = null;

             sessionSm.removeSessionWorker(sess);
          }

          res[0] = authResult;
          res[1] = authAction;
          res[2] = appServer;
          res[3] = newContext;
          res[4] = sess;
          return res;
       }

       /**
        * Detect if total number of simultaneous sessions from the given user account is more than the
        * configured per-account limit.
        *
        * @param    sc
        *           The current security cache.
        * @param    uid
        *           The userid of the interactive account
        *
        * @return   {@code true} if a user limit is NOT exceeded.
        */
       private boolean checkUserAccountLimit(SecurityCache sc, String uid)
       {
          int limit = sc.getMaxSessionsPerUser();

          if (limit >= 0)
          {
             synchronized (userLimitLock)
             {
                AtomicInteger num = interactiveSessions.get(uid);

                if (num != null && num.intValue() >= limit)
                {
                   LOG.severe(String.format("No additional interactive sessions are available for user account " +
                                      "'%s' (limit = %d, actual = %d).",
                                      uid,
                                      limit,
                                      num.intValue()));
                   return false;
                }
             }
          }

          return true;
       }

       /**
        * Detect if the total number of interactive user accounts in use exceeds the allowed number of
        * interactive users.
        *
        * @param    sc
        *           The current security cache.
        * @param    uid
        *           The userid of the interactive account
        *
        * @return   {@code true} if the max user limit is NOT exceeded.
        */
       private boolean checkMaxSystemUsers(SecurityCache sc, String uid)
       {
          int limit = sc.getMaxInteractiveUsers();

          if (limit >= 0)
          {
             synchronized (userLimitLock)
             {
                AtomicInteger num = interactiveSessions.get(uid);

                // users that already have an active session are already included in the interactiveUsers count
                // and should not be checked again
                if (num == null || num.intValue() == 0)
                {
                   if (interactiveUsers >= limit)
                   {
                      LOG.severe(String.format("No additional interactive sessions are available for this system " +
                                         "(limit = %d, actual = %d).",
                                         limit,
                                         interactiveUsers));
                      return false;
                   }
                }
             }
          }

          return true;
       }

       /**
        * Get the peer (remote) socket for this session.
        *
        * @return   See above.
        */
       private NetSocket getPeerSocket()
       {
          SecurityContextStack ctx = SecurityContextStack.getContext();
          SecurityContext key = ctx.getSessionKey();

          SecuritySession sess = sessionSm.locateSession(key);
          Object sessid = sess.getSessionId();

          return (NetSocket) sessid;
       }

       /**
        * Implements concurrent security cache refresh function.
        */
       private class SecurityCacheRefresh
       implements Runnable
       {
          /** logging mode for the new instance of cache */
          private int lm = 0;

          /** buffer to receive cache creation messages */

          private List<String[]> msgBuf = null;

          /**
           * Serves as the entry point for a separate thread.
           *
           * @param lm
           *        logging mode: one of {LM_ADMIN, LM_CONSOLE, LM_DUAL}
           */
          private SecurityCacheRefresh(int lm, List<String[]> msgBuf)
          {
             this.lm = lm;
             this.msgBuf = msgBuf;
          }

          /**
           * Serves as the entry point for a separate thread.
           */
          public void run()
          {
             // Safely query cached data
             SecurityCache sc = getCache();

             // establish initial security context
             SecurityContext context = sc.getInitialSecurityContext();
             contextSm.logContext(sc, context);
             SecurityContextStack.setContext(new SecurityContextStack(context));
             contextSm.assignContext(context);

             DirectoryService ds = DirectoryService.getInstance();
             ds.bind();

             // instantiate the security cache
             SecurityCache newCache = null;

             synchronized (SecurityManager.this)
             {
                try
                {
                   newCache = new SecurityCache(cfg, cacheSerial + 1, lm, msgBuf);

                   // only increment this value AFTER the security cache has been created successfully
                   cacheSerial++;

                   LOG.warning("Security Cache generation " + cacheSerial + " created.");
                }
                catch (Exception exc)
                {
                   LOG.warning("Security Cache generation " + (cacheSerial + 1) + " failed; ", exc);
                }

                if (newCache != null)
                {
                   SecurityCache oldCache = getCache();
                   Audit audit = oldCache.getAudit();

                   // synchronization below is required between this block and
                   // start(), stop() and log() methods in Audit, so that there is
                   // no interruption in audit logging

                   synchronized (audit)
                   {
                      audit.stop();
                      setCache(newCache);
                      audit = newCache.getAudit();
                      try
                      {
                         audit.start();
                      }
                      catch (IOException exc)
                      {
                         // The newly defined audit log file can't be activated and
                         // the old audit log's gone.
                         LOG.severe("*** Audit log failure *** ", exc);
                      }
                   }

                   SecurityContext ctx = newCache.getInitialSecurityContext();
                   contextSm.logNewContext(newCache, ctx);

                   newCache = null;
                }
             }

             ds.unbind();
             contextSm.unassignContext(context);
          }
       }

       /**
        * Implements password change that requires the privilege of running within
        * the initial security context.
        */
       private class PasswordChange
       implements Runnable
       {
          /* user account ordinal to change password for */
          private int ord = -1;

          /* new password as plain text */
          private String newPassword = null;

          /* the result of the password change */
          boolean result = false;

          /**
           * Constructor.
           *
           * @param ord
           *        user account ordinal to change password for
           * @param newPassword
           *        new password as plain text
           */
          PasswordChange(int ord, String newPassword)
          {
             this.ord = ord;
             this.newPassword = newPassword;
             this.result = false;
          }

          /**
           * Serves as the entry point for a separate thread.
           */
          public void run()
          {
             synchronized (SecurityManager.this)
             {
                SecurityCache sc = getCache();

                // establish initial security context
                SecurityContext context = sc.getInitialSecurityContext();
                contextSm.logContext(sc, context);
                SecurityContextStack.setContext(new SecurityContextStack(context));
                contextSm.assignContext(context);

                // hash the password
                byte[] hash = HashPassword.hashPassword(newPassword);

                // access the user account
                UserAccount ua = (UserAccount)sc.getAccountByOrd(ord);
                String uid = ua.getSubjectId();

                // change the password in the P2J directory
                String path = "/security/accounts/users/" + uid;

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

                // is the directory service is unavailable?
                if (ds == null)
                {
                   // done!
                   return;
                }

                // try to bind
                if (!ds.bind())
                {
                   // can't access the directory
                   return;
                }

                // any failure from here out, triggers an unbind if needed
                Date date = new Date();
                DateValue cdate = new DateValue(date);
                TimeValue ctime = new TimeValue(date);

                try
                {
                   if (!ds.openBatch(path))
                      throw new RuntimeException();

                   // any failure from here out, triggers a rollback
                   try
                   {
                      // set password hash
                      if (!ds.setNodeByteArray(path, "password", hash))
                      {
                         if (!ds.addNodeByteArray(path, "password", hash))
                            throw new RuntimeException();
                      }

                      // set password modification date
                      if (!ds.setNodeDate(path, "pwsetdate", cdate))
                      {
                         if (!ds.addNodeDate(path, "pwsetdate", cdate))
                            throw new RuntimeException();
                      }

                      // set password modification time
                      if (!ds.setNodeTime(path, "pwsettime", ctime))
                      {
                         if (!ds.addNodeTime(path, "pwsettime", ctime))
                            throw new RuntimeException();
                      }

                      // commit the changes
                      if (!ds.closeBatch(true))
                         throw new RuntimeException();

                      // change the password in the cache
                      ua.setPassword(hash, cdate, ctime);

                      result = true;
                   }
                   catch (Exception exc)
                   {
                      // rollback the modification
                      ds.closeBatch(false);
                   }
                }
                catch (Exception exc)
                {
                   // just allow silent return
                }
                finally
                {
                   // last use of the directory, end our session with it
                   ds.unbind();
                }
             }
          }
       }

       /**
        * This class extends <code>ClassLoader</code> to enable the loading of
        * a class file stored as a byte array in memory.
        */
       private class HookClassLoader
       extends ClassLoader
       {
          /** Byte array to load the class from */
          private byte[] data = null;

          /**
           * The class defined by the bytecode; will be loaded on the first {@link #findClass}
           * call.
           */
          private Class<?> cls = null;

          /**
           * Constructor for a single source.
           *
           * @param    parent
           *           The parent {@link ClassLoader}.
           * @param    source
           *           array of bytes this class loader operates
           */
          private HookClassLoader(ClassLoader parent, byte[] source)
          {
             super(parent);

             data = source;
          }

          /**
           * Find and load the class with the given name. If this the hook class was not yet defined,
           * define it first.
           * <p>
           * If the hook class name matches the received name, return the {@link #cls hook class}.
           * Else, let the parent class loader resolve it.
           *
           * @return   The {@link Class} object.
           *
           * @throws   ClassNotFoundException
           *           If the class can not be found or class linkage has error
           *           occurred.
           */
          @Override
          protected Class<?> findClass(String name)
          throws ClassNotFoundException
          {
             if (cls == null)
             {
                try
                {
                   cls = defineClass(name, data, 0, data.length);
                }
                catch (Exception e)
                {
                   throw new ClassNotFoundException("Could not load the custom hook class", e);
                }
             }

             return (name.equals(cls.getName()) ? cls : super.findClass(name));
          }
       }

       /** Wrapper class for Context related logic. */
       public class ContextSecurityManager
       {
          /**
           * Mark this context in headless mode.
           */
          public void setHeadless()
          {
             addToken(ContextKey.HEADLESS_KEY, Boolean.TRUE);
          }

          /**
           * Adds a token to the context map.
           *
           * @param key
           *        token key to be used as a key in the map
           *
           * @param token
           *        an arbitrary object to be kept in the entry
           *
           * @return <code>true</code> if the token is added successfully
           */
          boolean addToken(ContextKey key, Object token)
          {
             SecurityContextStack ctx = SecurityContextStack.getContext();
             return (ctx != null && ctx.addToken(key, token));
          }

          /**
           * Checks whether the specified token is in the context map.
           *
           * @param key
           *        token key to be used as a key in the map
           *
           * @return <code>true</code> if the token is found
           */
          public boolean hasToken(ContextKey key)
          {
             SecurityContextStack ctx = SecurityContextStack.getContext();
             return (ctx != null && ctx.hasToken(key));
          }

          /**
           * Gets the token from the context map.
           *
           * @param key
           *        token key to be used as a key in the map
           *
           * @return the value of the token as a generic object or <code>null</code>
           */
          public Object getToken(ContextKey key)
          {
             SecurityContextStack ctx = SecurityContextStack.getContext();
             return (ctx == null ? null : ctx.getToken(key));
          }

          /**
           * Remove a token from the context map.
           *
           * @param key
           *        token key to be used as a key in the map
           *
           * @return <code>true</code> if the token was removed successfully
           */
          boolean removeToken(ContextKey key)
          {
             SecurityContextStack ctx = SecurityContextStack.getContext();
             return (ctx != null && ctx.removeToken(key));
          }

          /**
           * Detects if there is a valid context associated with the current thread.
           *
           * @return   <code>true</code> if there is a valid security context.
           */
          public boolean hasContext()
          {
             return SecurityContextStack.getContext() != null;
          }

          /**
           * Assign the current thread to the given security context, and add a
           * unique identifier for this thread to the thread ID map.
           *
           * @param   context
           *          Security context to be assigned.
           */
          private void assignContext(SecurityContext context)
          {
             context.assign();
          }

          /**
           * Unassign the current thread from the given security context, and remove
           * its entry from the thread ID map.
           *
           * @param   context
           *          Security context to be unassigned.
           */
          private boolean unassignContext(SecurityContext context)
          {
             return context.unassign();
          }

          /**
           * Reset the effective security context associated with the current thread.
           *
           * @throws   RestrictedUseException
           *           If called outside of expected code.
           */
          public void resetContext()
          throws RestrictedUseException
          {
             // restrict the use of this method
             String[] callers = new String[]
                {
                   "com.goldencode.p2j.security.MultiSessionAppserverSecurityManager.setContext",
                   "com.goldencode.p2j.util.Agent$ResetContextCommand.execute"
                };
             SecurityUtil.checkCallerAbort(callers);

             SecurityContextStack.getContext().getEffectiveContext().reset();
          }

          /**
           * Initializes security context switching for the calling thread.
           * This call is supposed to be repeatable for permanent threads to allow
           * proper security cache refresh function.
           * <p>
           * This call immediately exits if called from the client.
           * <p>
           * If this call is made repeatedly, it checks whether the security cache is
           * refreshed compared to the generation of the cache used to create the
           * initial security context currently in use. If this is the case, the old
           * security context is released and replaced with the current one.
           * <p>
           * <i><u>Restricted use</u></i>. This method checks the caller to be:
           * <ul>
           *  <li> com.goldencode.p2j.net.RouterSessionManager.setInitialContext()
           *  <li> com.goldencode.p2j.net.StandardServer.bootstrap()
           *  <li> com.goldencode.p2j.net.StandardServer$1.run()
           * </ul>
           *
           * @throws RestrictedUseException if called improperly
           */
          public void setInitialSecurityContext()
          throws RestrictedUseException
          {
             // optimally, this would be done after the checkCaller below, but that
             // call is very expensive and by putting this here we allow the call
             // to this method but the caller gets no service so security is not
             // really reduced
             if (!cfg.isServer())
                return;

             // restrict the use of this method
             String[] callers = new String[]
                {
                   "com.goldencode.p2j.net.RouterSessionManager.setInitialContext",
                   "com.goldencode.p2j.main.StandardServer.bootstrap",
                   "com.goldencode.p2j.main.StandardServer$1.run",
                   "com.goldencode.p2j.main.ServerDriver.connect"
                };
             SecurityUtil.checkCallerAbort(callers);

             setInitialSecurityContextWorker(false, null);
          }

          /**
           * Initializes security context switching for the calling thread making
           * sure that the context is newly created and is not shared with any other
           * thread. The context will not be associated with any specific account
           * without a subsequent authentication. This is different than standard
           * server threads which have an initial context that is shared with other
           * server threads AND which are naturally associated with the server's
           * account (and thus with the server's rights).
           * <p>
           * This call is supposed to be repeatable for permanent threads to allow
           * proper security cache refresh function.
           * <p>
           * This call immediately exits if called from the client.
           * <p>
           * If this call is made repeatedly, it checks whether the security cache is
           * refreshed compared to the generation of the cache used to create the
           * initial security context currently in use. If this is the case, the old
           * security context is released and replaced with the current one.
           * <p>
           * <i><u>Restricted use</u></i>. This method checks the caller to be:
           * <ul>
           *  <li> com.goldencode.p2j.main.ServerDriver$SingleProcessClient.run()
           * </ul>
           *
           * @throws RestrictedUseException if called improperly
           */
          public void setUniqueInitialSecurityContext()
          throws RestrictedUseException
          {
             // optimally, this would be done after the checkCaller below, but that
             // call is very expensive and by putting this here we allow the call
             // to this method but the caller gets no service so security is not
             // really reduced
             if (!cfg.isServer())
                return;

             // restrict the use of this method
             String[] callers = new String[]
                {
                   "com.goldencode.p2j.main.ServerDriver$SingleProcessClient.run",
                   "com.goldencode.p2j.security.SecurityManager.authenticateServer",
                   "com.goldencode.p2j.admin.server.AuthFilter.doFilter",
                   "com.goldencode.p2j.security.ContextSwitcher.setContext",
                   "com.goldencode.p2j.security.MultiSessionAppserverSecurityManager.setInitialContext"
                };
             SecurityUtil.checkCallerAbort(callers);

             setInitialSecurityContextWorker(true, null);
          }

          /**
           * Initializes the security context switching for the calling thread using the given context
           * stack as the source of the initial and effective contexts.
           * <p>
           * This does not check or enforce session limits (e.g. max number of user sessions) because it relies
           * upon an already existing security context.  In other words, we are not adding a new user-level session
           * but instead we are just properly initializing additional threads.
           *
           * @param    stack
           *           Context stack to use instead of creating a new one. This allows the caller to
           *           override the initial and effective contexts.
           * @param    create
           *           {@code true} to add a new session for the effective context.  The new session
           *           will not be associated with any network session nor will there be an
           *           authentication process.  Instead, the previously authenticated effective context
           *           will be used to pattern a new session.
           */
          void forceSecurityContext(SecurityContextStack stack, boolean create)
          {
             if (create)
             {
                SecurityCache sc = getCache();

                // morph the session token
                SecurityContext ctx = stack.getEffectiveContext();
                ctx.morphSessionId(sessionSm.getNextUserSessionID());

                // the session id used here is the thread object, not the id used in the session token
                sessionSm.addSession(Thread.currentThread(), null, sc, ctx);
             }

             setInitialSecurityContextWorker(false, stack);
          }

          /**
           * Drops any current context (if any) and establishes an initial context
           * for the thread.
           *
           * @param    unique
           *           <code>true</code> to instantiate a new context that is not
           *           associated with the current server account and does not share
           *           any context-local data. <code>false</code> to get the standard
           *           server-wide initial context which is associated with the
           *           server's account AND shares context-local data with all other
           *           server threads that also share this initial context. This
           *           parameter is only honored if the <code>context</code> parameter
           *           is <code>null</code>.
           * @param    stack
           *           Optional context stack to use instead of creating a new one.
           *           This allows the caller to override the initial and effective
           *           contexts.
           */
          private void setInitialSecurityContextWorker(boolean              unique,
                                                       SecurityContextStack stack)
          {
             int oldSerial = -1;

             // access the current context stack
             SecurityContextStack cts = SecurityContextStack.getContext();

             // if there is currently a context associated, get the serial number
             if (cts != null)
             {
                oldSerial = cts.getCache().getSerial();
             }

             // safely query the current cache
             SecurityCache sc = getCache();

             // (re)establish initial security context (this occurs when there is
             // no context or when the context was defined in an older generation
             // of the cache)
             if (sc.getSerial() != oldSerial)
             {
                if (oldSerial != -1)
                   dropInitialSecurityContext();

                SecurityContext initial   = null;
                SecurityContext effective = null;

                if (stack == null)
                {
                   initial = unique ? sc.getUniqueInitialSecurityContext()
                      : sc.getInitialSecurityContext();

                   stack = new SecurityContextStack(initial);
                }
                else
                {
                   initial   = stack.getInitial();
                   effective = stack.getEffectiveContext();
                }

                // sets the thread-local pointer to the stack
                SecurityContextStack.setContext(stack);

                // assigns the initial context
                logContext(sc, initial);
                assignContext(initial);

                // update reference count etc for the effective context if needed
                if (effective != null && !effective.isInitial())
                {
                   logContext(sc, effective);
                   assignContext(effective);
                }

                DirectoryService ds = DirectoryService.getInstance();
                ds.bind();
             }
          }

          /**
           * Releases the initial security context assigned to the calling thread.
           * The initial security context can only be released if it is currently
           * assigned and is the only context in the stack.
           * <p>
           * If called improperly, has no effect.
           */
          public void dropInitialSecurityContext()
          {
             if (!cfg.isServer())
                return;

             // access the current context stack
             SecurityContextStack cts = SecurityContextStack.getContext();
             if (cts == null)
                return;

             SecurityContext ctx = cts.getEffectiveContext();
             if (!ctx.isInitial())
                return;

             // safely query cached data and log the context switch event
             SecurityCache sc = getCache();
             logContext(sc, null);

             // drop the initial security context
             SecurityContextStack.setContext(null);
             unassignContext(ctx);
          }

          /**
           * Initializes and returns a valid SSL environment.
           *
           * @return  The SSL environment which has been configured with our
           *          custom key and trust stores.
           */
          public SSLContext getSecureSocketContext()
          throws NoSuchAlgorithmException,
                 KeyStoreException,
                 UnrecoverableKeyException,
                 KeyManagementException,
                 ConfigurationException,
                 NoSuchProviderException
          {
             return getSecureSocketContext(cfg);
          }

          /**
           * Initializes and returns a valid SSL environment.
           *
           * @param   config
           *          Bootstrap configuration information used to initialize the
           *          secure socket context.
           *
           * @return   The SSL environment which has been configured with our
           *           custom key and trust stores.
           */
          public SSLContext getSecureSocketContext(BootstrapConfig config)
          throws NoSuchAlgorithmException,
                 KeyStoreException,
                 UnrecoverableKeyException,
                 KeyManagementException,
                 ConfigurationException,
                 NoSuchProviderException
          {
             SSLContext ctx = SecureSocketsRegistrar.register(config, true);

             // configure the SSL environment with our custom keys/certs
             TransportSecurity ts = certificateSm.getTransportSecurity(config);
             ts.attach(ctx);

             SSLParameters params = ctx.getSupportedSSLParameters();
             params.setCipherSuites(filterCiphers(params.getCipherSuites()));

             return ctx;
          }

          /**
           * Temporarily switches to a user context to serve the associated request.
           * The calling thread has to have the initial security context.
           * <p>
           * The use count for the target security context is incremented by 1.
           *
           * @param    key
           *           security context key
           *
           * @throws   RestrictedUseException
           *           If called improperly.
           */
          public void pushAndSwitchSecurityContext(Object key)
          throws RestrictedUseException
          {
             if (!cfg.isServer())
                return;

             // restrict the use of this method
             String[] callers = new String[]
                {
                   "com.goldencode.p2j.net.BaseSession.setContext",
                   "com.goldencode.p2j.security.MultiSessionAppserverSecurityManager.setContext"
                };
             SecurityUtil.checkCallerAbort(callers);

             pushContextWorker(key);
          }

          /**
           * Temporarily switches to a user context to serve the associated request.
           * The calling thread has to have the initial security context.
           * <p>
           * The use count for the target security context is incremented by 1.
           *
           * @param    sessionId
           *           session id
           *
           * @throws   RestrictedUseException
           *           If called improperly.
           */
          public void pushAndSwitchSecurityContextBySessionId(Object sessionId)
          throws RestrictedUseException
          {
             if (!cfg.isServer())
                return;

             // restrict the use of this method
             String[] callers = new String[]
                {
                   "com.goldencode.p2j.admin.server.AuthFilter.doFilter",
                   "com.goldencode.p2j.admin.server.AuthServiceImpl.login",
                   "com.goldencode.p2j.security.ContextSwitcher.setContext"
                };
             SecurityUtil.checkCallerAbort(callers);

             SecuritySession session = sessionSm.locateSessionById(sessionId);
             if (session == null)
             {
                throw new IllegalStateException("unknown session");
             }
             pushContextWorker(session.getSecurityContext());
          }

          /**
           * Temporarily switches to a user context to serve the associated request.
           * The calling thread has to have the initial security context.
           * <p>
           * The use count for the target security context is incremented by 1.
           *
           * @param    key
           *           security context key
           *
           * @throws   RestrictedUseException
           *           If called improperly.
           */
          void pushContextWorker(Object key)
          throws RestrictedUseException
          {
             // check the passed object
             if (!(key instanceof SecurityContext))
                throw new RestrictedUseException("invalid security context key");

             // check the current context
             SecurityContext ctx = (SecurityContext)key;
             SecurityContextStack cts = SecurityContextStack.getContext();
             if (cts == null)
                throw new RestrictedUseException("no initial context established");

             if (!cts.push(ctx))
                throw new RestrictedUseException("illegal context switch");

             // safely query cached data and log the context switch event
             SecurityCache sc = getCache();
             logContext(sc, ctx);

             assignContext(ctx);
          }

          /**
           * Restores the initial security context of the calling thread when it is
           * done with the user request.  The calling thread has to have its security
           * context switched to a user context prior to this call.
           * <p>
           * The use count for the current security context is decremented by 1.
           * If it becomes 0, it gets deleted immediately.
           *
           * @throws   RestrictedUseException
           *           If called from an unauthorized location.
           */
          public void popAndRestoreSecurityContext()
          throws RestrictedUseException
          {
             if (!cfg.isServer())
                return;

             // restrict the use of this method
             String[] callers = new String[]
                {
                   "com.goldencode.p2j.net.RouterSessionManager.restoreContext",
                   "com.goldencode.p2j.admin.server.AuthFilter.doFilter",
                   "com.goldencode.p2j.admin.server.AuthServiceImpl.login",
                   "com.goldencode.p2j.security.ContextSwitcher.releaseContext",
                   "com.goldencode.p2j.security.MultiSessionAppserverSecurityManager.setContext",
                };
             SecurityUtil.checkCallerAbort(callers);

             popContextWorker();
          }

          /**
           * Terminates all security contexts associated with the current thread,
           * including the initial context. This MUST be used when terminating
           * contexts obtained with {@link #setUniqueInitialSecurityContext}. If this
           * is not used, then the unique initial contexts will never be cleaned up
           * and there will be a memory leak.
           * <p>
           * The use count for the all security contexts is decremented by 1.
           * If it becomes 0, it gets deleted immediately.
           *
           * @throws   RestrictedUseException
           *           If called from an unauthorized location.
           */
          void popAllSecurityContext()
          throws RestrictedUseException
          {
             if (!cfg.isServer())
                return;

             SecurityContextStack cts = SecurityContextStack.getContext();

             if (cts == null)
                throw new RestrictedUseException("no security context established");

             SecurityContext ctx = cts.getEffectiveContext();

             if (ctx == null)
                throw new RestrictedUseException("illegal context switch");

             // exit any non-initial context using the normal worker
             if (!ctx.isInitial())
             {
                popContextWorker();
                ctx = cts.getInitial();
             }

             // exit the initial context
             endContext(cts, ctx);
          }

          /**
           * Restores the initial security context of the calling thread when it is
           * done with the user request.  The calling thread has to have its security
           * context switched to a user context prior to this call.
           * <p>
           * The use count for the current security context is decremented by 1.
           * If it becomes 0, it gets deleted immediately.
           *
           * @throws   RestrictedUseException
           *           If called improperly.
           */
          private void popContextWorker()
          throws RestrictedUseException
          {
             SecurityContextStack cts = SecurityContextStack.getContext();

             if (cts == null)
                throw new RestrictedUseException("no security context established");

             SecurityContext ctx = cts.pop();

             if (ctx == null)
                throw new RestrictedUseException("illegal context switch");

             endContext(cts, ctx);
          }

          /**
           * Worker routine to centralize unassignment and cleanup (if this was the
           * final usage) of the given context.
           *
           * @param    cts
           *           The context stack for the current thread.
           * @param    ctx
           *           The context to end.
           */
          private void endContext(SecurityContextStack cts, SecurityContext ctx)
          {
             // unassign context and do cleanup if it's time
             boolean finalUse = unassignContext(ctx);

             if (finalUse)
             {
                // IMPORTANT NOTE:
                // The simpler approach seems to be synchronizing on the
                // SecurityContext instance itself (and making any other conflicting
                // threads wait until cleanup is done), but this must not be done,
                // as it will deadlock.
                // Consider this scenario: Conversation thread starts the context
                // cleanup code (acquiring a lock on the SecurityContext instance).
                // The cleanup code for ConnectionManager will send the command to
                // end the virtual session and will wait for this command to
                // complete. When the Reader thread for the virtual session notices
                // that it needs to end, it will stop the queue and will AGAIN start
                // the context cleanup code for the same SecurityContext instance
                // (as the Reader for the virtual session is closed using the context
                // of the initiator). As the Conversation thread has a lock on this,
                // Reader can not go forward until Conversation thread releases the
                // lock. But Conversation thread is still in waiting mode to receive
                // an answer for its "end virtual session" command... so, deadlock.

                boolean clean = true;
                Object lock = ctx.getCleanupLock();
                synchronized (cleanupLocks)
                {
                   clean = (cleanupLocks.get(lock) == null);
                   if (clean)
                   {
                      cleanupLocks.put(lock, true);
                   }
                }

                if (clean)
                {
                   // context cleanup code is invoked only if we are the first thread
                   // which executes this code; else, skip it, as another thread is
                   // doing this work
                   try
                   {
                      // non-initial contexts have already been popped by this time,
                      // push it back to ensure the context is valid for the lifespan
                      // of cleanup
                      if (!ctx.isInitial())
                      {
                         cts.push(ctx);
                      }

                      ctx.cleanup();

                      if (!ctx.isInitial())
                      {
                         cts.pop();
                      }

                      logGoingContext(ctx);
                   }

                   finally
                   {
                      synchronized (cleanupLocks)
                      {
                         cleanupLocks.remove(lock);
                      }
                   }
                }
             }

             // safely query cached data and log the context switch event
             SecurityCache sc = getCache();
             logContext(sc, ctx);
          }

          /**
           * Private worker method that creates a new security context.
           *
           * @param  sc
           *         <code>SecurityCache</code> where new security context belongs
           * @param  pid
           *         process ID for new security context
           * @param  uid
           *         user ID for new security context

           * @return newly created <code>SecurityContext</code>

           * @throws   RestrictedUseException
           *           If called from unexpected caller.
           */
          SecurityContext createSecurityContext(SecurityCache sc, String pid, String uid)
          throws RestrictedUseException
          {
             // restrict the use of this method
             String[] callers = new String[]
                {
                   "com.goldencode.p2j.security.SecurityManager.authenticateRemote",
                   "com.goldencode.p2j.security.SecurityManager.postAuthenticateWorker",
                   "com.goldencode.p2j.security.SecurityManager.assignContext",
                   "com.goldencode.p2j.security.SecurityManager.authenticateRemote(String)",
                   "com.goldencode.p2j.security.SecurityManager$ContextSecurityManager.createSecurityContext",
                   "com.goldencode.p2j.security.MultiSessionAppserverSecurityManager.createContext"
                };
             SecurityUtil.checkCallerAbort(callers);

             int procI = sc.getOrdinalById(pid);
             int userI = sc.getOrdinalById(uid);

             // disallow access if both accounts are invalid
             if (procI == -1 && userI == -1)
             {
                return null;
             }

             int[] grpsI = null;

             UserAccount uacc = (UserAccount)sc.getAccountByOrd(userI);

             if (uacc != null)
             {
                grpsI = uacc.getGroups();
             }

             int    nres    = sc.getNumResources();
             int    sid     = sessionSm.getNextUserSessionID();
             String subject = (userI != -1) ? uid : pid;

             SecurityContext ctx = new SecurityContext(sc, procI, userI, grpsI, nres, sid, subject);
             logNewContext(sc, ctx);

             return ctx;
          }

          /**
           * Creates an audit record for security context creation event.
           *
           * @param  cache
           *         security cache where to log this event
           *
           * @param  ctx
           *         security context
           */
          private void logNewContext(SecurityCache cache, SecurityContext ctx)
          {
             Audit audit = cache.getAudit();
             if (!audit.isEnabled())
             {
                return;
             }

             StringBuilder sb = new StringBuilder();
             int[] checkList = ctx.getCheckList();

             for (int i = 0; i < checkList.length; i++)
             {
                sb.append(checkList[i]);
                sb.append("=");
                sb.append(cache.getAccountByOrd(checkList[i]).getSubjectId());
                sb.append(":");
             }
             sb.setLength(sb.length() - 1);

             audit.log(cache.getSystem(), "context",
                       SystemResource.SYSTEM_CONTEXT_CREATE, true, new String(sb));
          }

          /**
           * Creates an audit record for security context switch event.
           *
           * @param  cache
           *         security cache where to log this event
           *
           * @param  ctx
           *         coming security context, may be <code>null</code>
           */
          private void logContext(SecurityCache cache, SecurityContext ctx)
          {
             Audit audit = cache.getAudit();

             if (!audit.isEnabled())
             {
                return;
             }

             StringBuilder sb = new StringBuilder();
             int[] checkList = ctx == null ? new int[0] : ctx.getCheckList();

             for (int i = 0; i < checkList.length; i++)
             {
                sb.append(checkList[i]);
                sb.append("=");
                Account acc = cache.getAccountByOrd(checkList[i]);
                if (acc != null)
                {
                   sb.append(acc.getSubjectId());
                   sb.append(":");
                }
             }
             if (sb.length() > 0)
                sb.setLength(sb.length() - 1);

             audit.log(cache.getSystem(),
                       "context",
                       SystemResource.SYSTEM_CONTEXT_SWITCH,
                       true,
                       sb.toString());
          }

          /**
           * Creates an audit record for a terminated security context.
           *
           * @param  ctx
           *         security context
           */
          private void logGoingContext(SecurityContext ctx)
          {
             SecurityCache sc = getCache();
             Audit audit = sc.getAudit();
             if (!audit.isEnabled())
             {
                return;
             }

             StringBuilder sb = new StringBuilder();
             int[] checkList = ctx.getCheckList();

             for (int i = 0; i < checkList.length; i++)
             {
                sb.append(checkList[i]);
                sb.append("=");
                sb.append(getCache().getAccountByOrd(checkList[i]).getSubjectId());
                sb.append(":");
             }
             sb.setLength(sb.length() - 1);

             audit.log(sc.getSystem(), "context",
                       SystemResource.SYSTEM_CONTEXT_DELETE, true, new String(sb));
          }
       }

       /**
        * An interface to expose the {@link CertificateSecurityManager#getEncryptedKeyStoreWorker} to threads
        * which have proper server context.
        */
       @FunctionalInterface
       public static interface EncryptedKeyStoreFunction
       {
          /**
           * Get an encrypted key store with the SSL private key.  The key store and key entry are
           * encrypted using the specified passwords.
           *
           * @param    alias
           *           The alias.
           * @param    ksPassword
           *           Password used to encrypt the key store.
           * @param    kePassword
           *           Password used to encrypt the key entry within the store.
           * @param    type
           *           The store type
           *
           * @return   The encrypted key store or {@code null} if the alias does not exist or
           *           the store could not be generated.
           */
          public byte[] getEncryptedKeyStore(String alias,
                                             String ksPassword,
                                             String kePassword,
                                             Store  type);
       }

       /** Wrapper class for certificate / encryption related logic. */
       public class CertificateSecurityManager
       {
          /**
           * Parses a X.509 certificate and returns the CN component of the subject's
           * distinctive name.
           *
           * @param  x509
           *         certificate to parse
           * @return commonName part of the subject's distinctive name. May be empty.
           * @throws CertificateParsingException
           *         if DN is malformed or no CN was found in DN
           */
          String getSubjectCommonName(X509Certificate x509)
          throws CertificateParsingException
          {
             String name = x509.getSubjectX500Principal().getName();
             String comp = null;
             String cnam = null;
             String cval = null;
             int i = 0;
             int j = 0;

             // components of the DN are comma+spaces separated
             i = name.indexOf(',');
             if (i == -1)
                comp = name;
             else
                comp = name.substring(0, i);

             while (comp.length() > 0)
             {
                j = comp.indexOf('=');
                if (j == -1)
                   throw new CertificateParsingException("malformed subject DN");
                cnam = comp.substring(0, j).trim();
                cval = comp.substring(j + 1);

                // check for CN or COMMONNAME
                if (cnam.compareToIgnoreCase("cn")         == 0 ||
                   cnam.compareToIgnoreCase("commonName") == 0)
                   return cval;

                if (i == -1)
                   break;
                name = name.substring(i + 1);
                i = name.indexOf(',');
                if (i == -1)
                   comp = name;
                else
                   comp = name.substring(0, i);
             }

             throw new CertificateParsingException("malformed subject DN, no CN");
          }

          /**
           * Get an encrypted key store with the trusted certificates.
           *
           * @param    password
           *           The password used to encrypt the store.
           *
           * @return   The encrypted key store or {@code null} if the store could not be generated.
           */
          public byte[] getEncryptedTrustStore(String password)
          {
             if (!isServerAccount())
             {
                return null;
             }

             if (tranSec == null)
             {
                if (LOG.isLoggable(Level.SEVERE))
                {
                   String msg = "Trying to get a trust store, but no transport security is initialized.";
                   LOG.logp(Level.SEVERE, "SecurityManager", "getEncryptedTrustStore", msg);
                }

                return null;
             }

             ProcessAccount pc = getCache().getServerAccount();
             if (!pc.isMaster())
             {
                return null;
             }

             try
             {
                KeyStore ks = tranSec.getTrustStore();

                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ks.store(bos, password.toCharArray());

                return bos.toByteArray();
             }
             catch (Exception e)
             {
                final String msg = "Could not retrieve the server's trust store";
                LOG.logp(Level.SEVERE, "SecurityManager", "getEncryptedKeyStore", msg, e);
             }

             return null;
          }

          /**
           * Get a function worker to resolve an encrypted key store with the SSL private key.
           *
           * @return   A function reference to {@link #getEncryptedKeyStoreWorker} or <code>null</code>
           *           if the thread doesn't have a server context.
           */
          public EncryptedKeyStoreFunction getEncryptedKeyStore()
          {
             if (!isServerAccount())
             {
                return null;
             }

             ProcessAccount pc = getCache().getServerAccount();
             if (!pc.isMaster())
             {
                return null;
             }

             return this::getEncryptedKeyStoreWorker;
          }

          /**
           * Get an encrypted key store with the SSL private key.  The key store and key entry are
           * encrypted using the specified passwords.
           *
           * @param    alias
           *           The alias.
           * @param    ksPassword
           *           Password used to encrypt the key store.
           * @param    kePassword
           *           Password used to encrypt the key entry within the store.
           * @param    type
           *           The key store type
           *
           * @return   The encrypted key store or {@code null} if the alias does not exist or
           *           the store could not be generated.
           */
          private byte[] getEncryptedKeyStoreWorker(String alias,
                                                    String ksPassword,
                                                    String kePassword,
                                                    Store  type)
          {
             try
             {
                byte[] encrypted = getCache().getPrivateKey(alias);
                if (encrypted == null)
                {
                   return null;
                }

                String keyEntryPassword = getCache().getKeyEntryPassword(alias);
                if (keyEntryPassword == null)
                {
                   return null;
                }

                SSLCertFactory factory = getCache().getSSLCertFactory();
                PrivateKey pkey = factory.decryptPrivateKey(encrypted, keyEntryPassword);

                KeyStore ks = KeyStore.getInstance("JKS");
                ks.load(null, null);

                KeyStore keyStore;

                if (type == Store.TRUST_STORE)
                {
                   keyStore = getCache().getTrustStore();
                }
                else
                {
                   keyStore = getCache().getWebStore();
                }

                Certificate x509 = keyStore.getCertificate(alias);

                Certificate[] chain;

                if (type == Store.WEB_STORE)
                {
                   chain = keyStore.getCertificateChain(alias);
                }
                else
                {
                   // trust key store doesn't include the private key for this certificate
                   chain = new Certificate[] { x509 };
                }

                ks.setKeyEntry(alias, pkey, kePassword.toCharArray(), chain);

                ByteArrayOutputStream bos = new ByteArrayOutputStream();

                ks.store(bos, ksPassword.toCharArray());

                return bos.toByteArray();
             }
             catch (Exception e)
             {
                if (LOG.isLoggable(Level.SEVERE))
                {
                   final String msg = "Could not retrieve the encrypted key store for alias " + alias;
                   LOG.logp(Level.SEVERE, "SecurityManager", "getEncryptedKeyStore", msg, e);
                }
             }

             return null;
          }

          /**
           * Detects if server certificate validation is required.
           *
           * @param   config
           *          BootstrapConfig instance which should override the security
           *          manager's version;  <code>null</code> to use default config.
           *
           * @return  <code>true</code> if server certificate validation is
           *          required.
           */
          private boolean needsServerValidation(BootstrapConfig config)
          {
             if (config == null)
             {
                config = cfg;
             }

             String item = null;

             try
             {
                item = config.getConfigItem("security", "certificate", "validate");
             }

             catch (ConfigurationException cfg)
             {
                // assume no server cert validation
             }

             return "true".equals(item);
          }

          /**
           * Validate the server's certificate if this was specified in the
           * bootstrap configuration.
           *
           * @param   locSess
           *          The local SSL session to validate.
           * @param   config
           *          BootstrapConfig instance which should override the security
           *          manager's version;  <code>null</code> to use default config.
           */
          private boolean validateServer(SSLSession locSess, BootstrapConfig config)
          {
             java.security.cert.Certificate[] peerCerts = null;

             // TODO: is getPeerCertificates() required if needsServerValidation()
             //       returns false or can it be deferred?

             // query server's certificate
             try
             {
                peerCerts = locSess.getPeerCertificates();
             }
             catch (Exception e)
             {
                // exception is handled as if there was 0 certificates read
             }

             // the following server certificate validation steps
             // are taken only if implicitly or explicitly requested

             if (needsServerValidation(config))
             {
                // fail authentication if no server certificate is available
                if (peerCerts == null || peerCerts.length == 0)
                {
                   return false;
                }

                X509Certificate servCert = (X509Certificate) peerCerts[0];

                // validate the certificate
                try
                {
                   servCert.checkValidity();
                }

                catch (Exception e)
                {
                   return false;
                }

                // TODO: what is the difference between the checkValidity() above
                //       and this name based checking below, why are both done?
                //       (document this as it is not clear at all)

                List<String> names = null;

                // build a set of DNS names/common names to check
                try
                {
                   names = fetchAltNames(servCert, 2);
                }

                catch (CertificateParsingException e)
                {
                   // this exception can't be tolerated
                   return false;
                }

                // get CN if no altNames
                if (names.isEmpty())
                {
                   try
                   {
                      names.add(getSubjectCommonName(servCert));
                   }

                   catch (CertificateParsingException e)
                   {
                      // this exception can't be tolerated
                      return false;
                   }
                }

                // compare known names with the used hostname
                int i = hostMatch(locSess.getPeerHost(), names);

                if (i == -1)
                {
                   // all other types of checking failed, if a truststore alias
                   // was specified, then lookup the certificate by that alias and
                   // compare it with the server's certificate

                   TransportSecurity ts = null;
                   try
                   {
                      // if this is the default config, use the default transport
                      // security
                      ts = (config == cfg) ? tranSec
                         : getTransportSecurity(config);
                   }
                   catch (Exception e)
                   {
                      return false;
                   }

                   KeyStore trust = ts.getTrustStore();

                   try
                   {
                      String trustAlias = config.getConfigItem("security",
                                                               "truststore",
                                                               "alias");
                      X509Certificate trusted = (X509Certificate)
                         trust.getCertificate(trustAlias);

                      if (!servCert.equals(trusted))
                      {
                         return false;
                      }
                   }

                   catch (Exception e)
                   {
                      return false;
                   }
                }
             }

             return true;
          }

          /**
           * Creates an instance of <code>TransportSecurity</code> class that serves
           * the client or server end of secured TLS connections, as needed.  The
           * newly created instance is a singleton within the domain of this
           * instance of <code>SecurityManager</code>. Thus, repeatable calls return
           * the same reference.
           *
           * @return   The instance that configures the SSL environment with custom
           *           key stores and trust stores.
           */
          private synchronized TransportSecurity getTransportSecurity(BootstrapConfig config)
          throws NoSuchAlgorithmException,
                 KeyStoreException,
                 UnrecoverableKeyException,
                 ConfigurationException
          {
             TransportSecurity ts = tranSec;

             // if the default transport security was not initialized or a different
             // bootstrap config is used (i.e. not the default config), then create
             // the transport security instance
             if (tranSec == null || config != cfg)
             {
                boolean srv = config.isServer();

                KeyStore trust = null;

                if (srv)
                {
                   // safely query cached data
                   SecurityCache sc = getCache();

                   // obtain the cached trust store
                   trust = sc.getTrustStore();

                   if (config.getString("security", "keystore", "filename", null) == null)
                   {
                      // read the server's private key from the directory, if no file is specified
                      String alias = getServerAlias();

                      String ksPassword = RandomWordGenerator.create();
                      String kePassword = RandomWordGenerator.create();
                      byte[] store = getEncryptedKeyStoreWorker(alias,
                                                                ksPassword,
                                                                kePassword,
                                                                Store.TRUST_STORE);

                      if (store != null)
                      {

                         if (config.getConfigItem("access", "password", "keystore") == null)
                         {
                            config.setConfigItem("access", "password", "keystore", ksPassword);
                         }
                         if (config.getConfigItem("access", "password", "keyentry") == null)
                         {
                            config.setConfigItem("access", "password", "keyentry", kePassword);
                         }

                         config.setConfigItem("security", "keystore", "bytes",
                                              Base64.byteArrayToBase64(store));
                      }
                   }

                }

                ts = new TransportSecurity(config, srv, trust);

                if (config == cfg)
                {
                   // set the default transport security, if this is the default
                   // bootstrap config
                   tranSec = ts;
                }
             }

             return ts;
          }

          /**
           * Parses a X.509 certificate and returns a list of alternative names of a
           * specified type. The list may be empty if no such names are found.
           *
           * @param  x509
           *         certificate to parse
           * @param  altNameType
           *         integer type of alternative name to look for
           * @return <code>List</code> of found names as <code>String</code>s. May be
           *         empty.
           * @throws CertificateParsingException
           */
          private List<String> fetchAltNames(X509Certificate x509, int altNameType)
          throws CertificateParsingException
          {
             List<String> names = new LinkedList<String>();

             Collection<List<?>> col = x509.getSubjectAlternativeNames();
             if (col != null)
             {
                Iterator<List<?>> iter = col.iterator();
                List<?> l = null;
                Integer iType = null;

                while (iter.hasNext())
                {
                   // every item in the collection is a list with two elements
                   l = iter.next();
                   // first element of the list is an Integer entry type
                   iType = (Integer)l.get(0);
                   // if type is 2, the second element of the list is a name String
                   if (iType == altNameType)
                      names.add((String)l.get(1));
                }
             }

             return names;
          }
       }

       /** Wrapper class for session related logic. */
       public class SessionSecurityManager
       {
          /** List of sessions. */
          private List<SecuritySession> listOfSess = new LinkedList<>();

          /** Cache for quick retrieval based on the context's session token id
           * of the {@link SecuritySession}. The key should only be what is returned by
           * {@link SessionToken#getSessionID()}. */
          private final Map<Integer, SecuritySession> sessionByContextCache = new ConcurrentHashMap<>();

          /** Next available, unique user session ID. */
          private int nextUserSessionID = 0;

          /**
           * Get the number which uniquely identifies the current context, or
           * <code>null</code> if there is no current context.
           *
           * @return  Current security context's session ID.
           */
          public Integer getSessionId()
          {
             // Pick up the generation of security cache
             SecurityContextStack ctxs = SecurityContextStack.getContext();
             SecurityContext ctx = ctxs.getEffectiveContext();

             return (ctx != null) ? ctx.getSessionId() : null;
          }

          /**
           * Sets the uuid of the current session and the uuid of the original session (when forked).
           * @param   uuid
           *          The uuid of the session.
           * @param   relatedUuid
           *          The uuid of the original related session.
           * @param   osUserName
           *          The OS username.
           * @param   pid
           *          The pid.
           * @param   driverName
           *          The client driver name.
           * @param   webPort
           *          The web client http port.
           * @param   sessionDescription
           *          The session description from SsoAuthenticator.
           * @param   browserWebSocket
           *          The browser WebSocket address.
           */
          public SecuritySession addSessionDetails(String uuid,
                                                   String relatedUuid,
                                                   String osUserName,
                                                   long pid,
                                                   String driverName,
                                                   int webPort,
                                                   String sessionDescription,
                                                   String browserWebSocket)
          throws RestrictedUseException
          {
             SecurityUtil.checkCallerAbort( "com.goldencode.p2j.main.StandardServer.setupSession");
             SecurityContext effectiveContext = SecurityContextStack.getContext().getEffectiveContext();
             SecuritySession currentSession = locateSession(effectiveContext);
             if (currentSession != null)
             {
                currentSession.setUuid(uuid);
                currentSession.setRelatedSessionUuid(relatedUuid);
                currentSession.setOsUserName(osUserName);
                currentSession.setPid(pid);
                currentSession.setDriverName(driverName);
                currentSession.setHttpPort(webPort);
                currentSession.setBrowserWebSocket(browserWebSocket);
                currentSession.setDescription(sessionDescription);
             }
             return currentSession;
          }

          /**
           * Get the session token which uniquely identifies the current context, or
           * <code>null</code> if there is no current context.
           *
           * @return  Current security context's session token.
           */
          public SessionToken getSessionToken()
          {
             // Pick up the generation of security cache
             SecurityContextStack ctxs = SecurityContextStack.getContext();
             SecurityContext ctx = ctxs.getEffectiveContext();

             return (ctx != null) ? ctx.getSessionToken() : null;
          }

          /**
           * Returns the session for the current context, or <code>null</code> if there is no current context.
           *
           * @return  Current security context's session.
           */
          public SecuritySession getCurrentSession()
          {
             SecurityContextStack ctx = SecurityContextStack.getContext();
             SecurityContext key = ctx.getSessionKey();
             return sessionSm.locateSession(key);
          }

          /**
           * 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.
           */
          SessionInfo[] getActiveSessions(String jar, String iface)
          {
             Object[] contexts = SessionManager.get().getSessionContexts(jar, iface);
             List<SessionInfo> data = new ArrayList<SessionInfo>(contexts.length);

             for (Object o : contexts)
             {
                SecuritySession sess = locateSession((SecurityContext) o);
                SessionInfo descr = getSessionDescriptor(sess);

                data.add(descr);
             }

             return data.toArray(new SessionInfo[0]);
          }

          /**
           * Locates the session in the list of sessions with matching security context.
           *
           * @param  context
           *         security context to look for
           * @return instance of <code>Session</code> associated with the security
           *         context
           */
          SecuritySession locateSession(SecurityContext context)
          {
             // purge the session data
             synchronized (listOfSess)
             {
                for (SecuritySession sess : listOfSess)
                {
                   if (sess.hasContext(context))
                      return sess;
                }
             }

             return null;
          }

          /**
           * Locates the session in the list of sessions with matching session id.
           *
           * @param  sessionId
           *         session id
           * @return instance of <code>Session</code> associated with the session id
           */
          SecuritySession locateSessionById(Object sessionId)
          {
             // purge the session data
             synchronized (listOfSess)
             {
                for (SecuritySession sess : listOfSess)
                {
                   if (Objects.equals(sessionId, sess.getSessionId()))
                   {
                      return sess;
                   }
                }
             }

             return null;
          }

          /**
           * Creates a report of all current user and process sessions.
           *
           * @return   Session descriptors suitable for reporting purposes.
           */
          SessionInfo[] getSessionReport()
          {
             List<SessionInfo> info = new ArrayList<SessionInfo>();

             synchronized (listOfSess)
             {
                for (SecuritySession sess : listOfSess)
                {
                   SessionInfo descr = getSessionDescriptor(sess);

                   // virtual session cannot be inspected at this time, so avoid
                   // listing virtual sessions
                   if (descr != null)
                      info.add(descr);
                }
             }

             return info.toArray(new SessionInfo[0]);
          }

          /**
           * 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 or
           *           <code>null</code> if the session matching the id was not found.
           */
          SessionInfo getSessionReportById(int id)
          {
             SecuritySession session = sessionByContextCache.get(id);
             if (session == null)
             {
                return null;
             }

             return getSessionDescriptor(session);
          }

          /**
           * Obtain a descriptor for the given session.  At this time virtual sessions
           * cannot be inspected.
           *
           * @param    session
           *           The session on which to report.  Must not be <code>null</code>.
           *
           * @return   The descriptor, suitable for reporting on the session details.
           *           <code>null</code> will be returned if the session cannot be
           *           inspected.
           *
           * @throws   NullPointerException
           *           If the given session is <code>null</code>.
           */
          public SessionInfo getSessionDescriptor(SecuritySession session)
          {
             if (session == null)
                throw new NullPointerException("Session may not be null!");

             Object    id     = session.getSessionId();
             NetSocket socket = null;

             if (id instanceof NetSocket)
             {
                socket = (NetSocket) id;
             }
             else
             {
                return null;
             }

             SessionInfo info = new SessionInfo();

             SecurityContext context = session.getSecurityContext();
             SecurityCache   cache   = session.getCache();

             // read the user ordinal
             int ord = context.getUserOrdinal();

             if (ord == -1)
             {
                // we aren't a user, perhaps we are a process?
                info.user = false;
                ord = context.getProcessOrdinal();
             }

             Account acct = cache.getAccountByOrd(ord);

             info.name      = acct.getSubjectId();
             info.descr     = acct.getDescription();
             info.id        = context.getSessionId();
             info.uuid      = session.getUid();
             info.relatedSessionUuid = session.getRelatedSessionUuid();
             info.osUser    = session.getOsUserName();
             info.driverType = session.getDriverName();
             info.sessionDescription = session.getDescription();
             info.httpPort  = session.getHttpPort();
             info.pid       = session.getPid();
             info.browserWebSocket = session.getBrowserWebSocket();
             info.remoteNet = socket.getRemoteSockAddr().toString();
             info.localNet  = socket.getLocalSockAddr().toString();
             info.secure    = (socket.getSession() != null);
             info.logonTime = session.getTimeStamp();
             info.cacheGen  = cache.getSerial();

             return info;
          }

          /**
           * Forcibly terminates (closes) the socket in use for the given session. At
           * this time it is not possible to terminate virtual sessions.
           *
           * @param    sid
           *           Session ID to terminate.
           *
           * @throws   IllegalStateException
           *           If the given session id is invalid, is associated with a
           *           virtual session or if it is associated with the current
           *           session being used.
           */
          public void killSession(int sid)
          throws RestrictedUseException
          {
             String[] callers = new String[]
                {
                   "com.goldencode.p2j.security.SecurityAdmin.killSession",
                   "com.goldencode.p2j.main.StandardServer.enforceOneBrowserPolicy"
                };
             SecurityUtil.checkCallerAbort(callers);

             NetSocket socket  = null;

             SecurityContextStack stack   = SecurityContextStack.getContext();
             SecurityContext      current = stack.getEffectiveContext();
             int                  thisId  = current.getSessionId();

             // make sure we don't kill the current/active session
             if (sid == thisId)
             {
                throw new IllegalStateException("You cannot terminate the session " +
                                                   "which you are currently using!");
             }

             synchronized (listOfSess)
             {
                for (SecuritySession session : listOfSess)
                {
                   SecurityContext context = session.getSecurityContext();

                   if (context.getSessionId() == sid)
                   {
                      Object id = session.getSessionId();

                      if (id instanceof NetSocket)
                      {
                         socket = (NetSocket) id;
                         break;
                      }
                      else
                      {
                         String spec = "Session ID %d cannot be killed. It may be a" +
                            " virtual session (not killable at this time).";
                         throw new IllegalStateException(String.format(spec, sid));
                      }
                   }
                }
             }

             if (socket == null)
             {
                throw new IllegalStateException(String.format("Invalid session ID %d!", sid));
             }

             // kill the socket, this will cause both sides to "unwind" and disconnect
             socket.close();
          }

          /**
           * Destroys the security context associated with the given key.
           * <p>
           * <i><u>Restricted use</u></i>. This method checks the caller to be:
           * <ul>
           *   <li> com.goldencode.p2j.net.BaseSession.cleanupContext()
           *   <li> com.goldencode.p2j.net.Queue.stop()
           * </ul>
           *
           * @param    key
           *           The security context.
           *
           * @throws   RestrictedUseException
           *           If called improperly from unauthorized code.
           */
          public void terminateSession(Object key)
          throws RestrictedUseException
          {
             // restrict the use of this method
             String[] callers = new String[]
                {
                   "com.goldencode.p2j.net.BaseSession.cleanupContext",
                   "com.goldencode.p2j.net.Queue.stop"
                };
             SecurityUtil.checkCallerAbort(callers);

             if (!(key instanceof SecurityContext))
             {
                throw new IllegalStateException("Invalid security context key.");
             }

             SecuritySession sess = removeSession((SecurityContext) key);
             if (sess == null)
             {
                return;
             }
             terminateSessionWorker(sess);
          }

          /**
           * Destroys the security context associated with the session id.
           * <p>
           * <i><u>Restricted use</u></i>. This method checks the caller to be:
           * <ul>
           *   <li> com.goldencode.p2j.admin.server.AuthServiceImpl.logout
           * </ul>
           *
           * @param    sessionId
           *           The session id.
           *
           * @throws   RestrictedUseException
           *           If called improperly from unauthorized code.
           */
          public void terminateSessionById(Object sessionId)
          throws RestrictedUseException
          {
             // restrict the use of this method
             String[] callers = new String[]
                {
                   "com.goldencode.p2j.admin.server.AuthServiceImpl.logout",
                   "com.goldencode.p2j.security.LegacyWebSecurityManager.destroyWebRequestContext",
                   "com.goldencode.p2j.web.GenericWebServer$SessionCleaner.sessionDestroyed"
                };
             SecurityUtil.checkCallerAbort(callers);

             if (sessionId == null)
             {
                throw new IllegalArgumentException();
             }

             SecuritySession sess = removeSessionById(sessionId);
             if (sess == null)
             {
                return;
             }
             terminateSessionWorker(sess);
          }

          /**
           * 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.
           */
          void terminateSessions(String jar, String iface)
          {
             try
             {
                SessionManager.get().terminateSessions(jar, iface);
             }
             catch (RestrictedUseException e)
             {
                throw new IllegalStateException("Call of " +
                                                   "SessionManager.terminateSessions from this function should" +
                                                   "be granted.");
             }
          }

          /**
           * Destroys the security context associated with the given session.
           *
           * @param    session
           *           The session to terminate.
           *
           * @throws   RestrictedUseException
           *           If called improperly from unauthorized code.
           */
          private void terminateSessionWorker(SecuritySession session)
          throws RestrictedUseException
          {
             // log the context deletion event
             SecurityContext ctx = session.getSecurityContext();

             if (ctx.isInitial())
                LOG.fine("*** terminating Initial Security Context ***");

             // unassign context and do cleanup if it's time
             boolean finalUse = contextSm.unassignContext(ctx);
             if (finalUse)
             {
                // ensure the context is valid for the lifespan of cleanup
                SecurityContextStack cts = SecurityContextStack.getContext();
                boolean useInitial = cts == null;
                if (useInitial)
                {
                   contextSm.setInitialSecurityContextWorker(false, null);
                   cts = SecurityContextStack.getContext();
                }

                try
                {
                   boolean clean = true;
                   Object lock = ctx.getCleanupLock();
                   synchronized (cleanupLocks)
                   {
                      clean = (cleanupLocks.get(lock) == null);
                      if (clean)
                      {
                         cleanupLocks.put(lock, true);
                      }
                   }

                   if (clean)
                   {
                      // context cleanup code is invoked only if we are the first thread
                      // which executes this code; else, skip it, as another thread is
                      // doing this work

                      // there are cases when there is already an additional context
                      // attached... and this call will fail. so, we pop the previous
                      // context before doing the cleanup
                      SecurityContext old = cts.pop();
                      try
                      {
                         cts.push(ctx);
                         ctx.cleanup();
                         ctx = cts.pop();

                         contextSm.logGoingContext(ctx);
                      }
                      finally
                      {
                         if (old != null)
                            cts.push(old);

                         synchronized (cleanupLocks)
                         {
                            cleanupLocks.remove(lock);
                         }
                      }
                   }
                }
                finally
                {
                   if (useInitial)
                   {
                      contextSm.dropInitialSecurityContext();
                   }
                }
             }
          }

          /**
           * Remove the session (which is associated with the given context) from the
           * master session list.
           *
           * @param    key
           *           The security context.
           *
           * @return   The session that was removed.
           */
          private SecuritySession removeSession(SecurityContext key)
          {
             // locate the session
             SecuritySession sess = locateSession(key);

             // purge the session data
             removeSessionWorker(sess);

             return sess;
          }

          /**
           * Remove the session (which is associated with the given session id) from the
           * master session list.
           *
           * @param    sessionId
           *           The session id.
           *
           * @return   The session that was removed.
           */
          private SecuritySession removeSessionById(Object sessionId)
          {
             // locate the session
             SecuritySession sess = locateSessionById(sessionId);

             // purge the session data
             removeSessionWorker(sess);

             return sess;
          }

          /**
           * Remove the given session from the session list.
           *
           * @param    session
           *           The session to remove.
           */
          private void removeSessionWorker(SecuritySession session)
          {
             synchronized (listOfSess)
             {
                listOfSess.remove(session);
             }

             sessionByContextCache.remove(session.getSecurityContext().getSessionId());
             if (session.isInteractive())
             {
                trackUserSession(session.getUserid(), false);
             }
          }

          /**
           * Retrieve a unique user session ID to assign to a new security context.
           *
           * @return  Unique session ID which is not the reserved, system ID.
           */
          private synchronized int getNextUserSessionID()
          {
             while (++nextUserSessionID == SecurityContext.SYSTEM_SESSION)
                ; //empty while body

             return nextUserSessionID;
          }

          /**
           * Create a new session object, add it to the session list and return it.
           *
           * @param    sid
           *           The unique session identifier object.
           * @param    uid
           *           If not {@code null} this is an interactive user session which must be tracked.
           * @param    sc
           *           The current security cache.
           * @param    cxt
           *           The context to associate with this session.
           *
           * @return   The created session.
           */
          private SecuritySession addSession(Object sid, String uid, SecurityCache sc, SecurityContext cxt)
          {
             SecuritySession session = new SecuritySession(sid, sc, cxt);

             synchronized (listOfSess)
             {
                listOfSess.add(session);
             }

             if (uid != null)
             {
                session.setUserid(uid);
                session.setInteractive();
                trackUserSession(uid, true);
             }

             sessionByContextCache.put(session.getSecurityContext().getSessionId(), session);
             return session;
          }

          /**
           * Maintain tracking state for interactive user sessions.  This state is used to implement user session
           * limits.
           *
           * @param    uid
           *           The userid of the interactive account
           * @param    add
           *           {@code true} for adding a new session and {@code false} for removing a session.
           */
          private void trackUserSession(String uid, boolean add)
          {
             synchronized (userLimitLock)
             {
                AtomicInteger num = interactiveSessions.get(uid);

                if (add)
                {
                   if (num == null)
                   {
                      // no sessions exist for this account
                      interactiveUsers++;

                      num = new AtomicInteger(1);
                      interactiveSessions.put(uid, num);
                   }
                   else
                   {
                      // there is already at least 1 session open for this account
                      num.incrementAndGet();
                   }
                }
                else
                {
                   if (num == null)
                   {
                      // we should never be called to remove a session that was not previously added, if we are
                      // here there is some kind of unbalanced add/remove
                      LOG.severe(String.format("Missing tracking state for interactive user '%s'.", uid));
                   }
                   else
                   {
                      if (num.decrementAndGet() == 0)
                      {
                         interactiveUsers--;
                         interactiveSessions.remove(uid);
                      }
                   }
                }
             }
          }
       }

       /** Wrapper class for virtual connection related logic. */
       public class VirtualConnectionSecurityManager
       {
          /**
           * Get the effective security context associated with the current thread,
           * if any.
           *
           * @return  Effective security context, or <code>null</code> if either
           *          this is not a server-side security manager, or there is no
           *          effective security context associated with the current
           *          thread.
           *
           * @throws  RestrictedUseException
           *          if called improperly.
           */
          public Object getEffectiveContext()
          throws RestrictedUseException
          {
             if (!cfg.isServer())
             {
                return null;
             }

             // restrict the use of this method
             String[] callers = new String[]
                {
                   "com.goldencode.p2j.net.RouterSessionManager.connectVirtual",
                };
             SecurityUtil.checkCallerAbort(callers);

             SecurityContextStack scs = SecurityContextStack.getContext();

             return (scs != null) ? scs.getEffectiveContext() : null;
          }

          /**
           * Returns the subject ID associated with the passed key.
           * <p>
           * The subject ID returned is in the form processID/userID. Either part can
           * be empty.
           * No group IDs are returned as they must be uniquely identifiable by
           * userID.
           * <p>
           * Note that the security cache has to be taken from the passed key which
           * is a SecurityContext, as the correct identity of the key is bound to the
           * security cache generation.
           * <p>
           * <i><u>Restricted use</u></i>. This method checks the caller to be
           * the "authenticateRemote" method of the
           * com.goldencode.p2j.net.Dispatcher class.
           *
           * @param  key
           *         a key to a security context to be queried
           *
           * @return subject ID
           *
           * @throws RestrictedUseException if called improperly
           */
          public String getIdentity(Object key)
          throws RestrictedUseException
          {
             // restrict the use of this method
             SecurityUtil.checkCallerAbort(
                "com.goldencode.p2j.net.RouterSessionManager.connectVirtual");

             if (!cfg.isServer())
             {
                return null;
             }

             // check the passed object
             if (!(key instanceof SecurityContext))
             {
                throw new RestrictedUseException("invalid security context key");
             }

             // Pick up the generation of security cache
             SecurityContext ctx = (SecurityContext)key;
             SecurityCache sc = ctx.getCache();

             int procOrd = ctx.getProcessOrdinal();
             String procId = null;
             if (procOrd != -1)
                procId = sc.getAccountByOrd(procOrd).getSubjectId();
             else
                procId = "";

             int userOrd = ctx.getUserOrdinal();
             String userId = null;
             if (userOrd != -1)
                userId = sc.getAccountByOrd(userOrd).getSubjectId();
             else
                userId = "";

             return procId + "/" + userId;
          }

          /**
           * Create a security context for the given identity.  This is allowed to be called only when
           * establishing a virtual session, on the requester side.
           *
           * @param    identity
           *           The 'process/user' encoded identity.
           *
           * @return   The security context for this identity.
           */
          public SecurityContext createSecurityContext(String identity)
          throws RestrictedUseException
          {
             // restrict the use of this method
             SecurityUtil.checkCallerAbort("com.goldencode.p2j.net.RouterSessionManager.connectVirtual");

             SecurityCache sc = getCache();

             // parse the identity
             String[] parts = identity.split("/");
             int len = parts.length;
             if (len < 1 || len > 2)
                return null;

             String pid = parts[0];
             String uid = (len == 2) ? parts[1] : null;

             if (pid == null && uid == null)
                return null;

             return contextSm.createSecurityContext(sc, pid, uid);
          }
       }

       /** Wrapper class for SSO related logic. */
       public class SsoSecurityManager
       {
          /** Logger. */
          private final CentralLogger LOG = CentralLogger.get(SsoSecurityManager.class);

          /**
           * Returns a flag indicating if SSO authenticator is provided.
           *
           * @return    See above.
           */
          public boolean isSsoEnabled()
          {
             return getSsoAuthenticator() != null;
          }

          /**
           * Get the SSO authentication provider
           *
           * @return    See above.
           */
          public SsoAuthenticator getSsoAuthenticator()
          {
             return getCache().getSsoHook();
          }

          /**
           * Creates a security context for the FWD account and assigns it to the current Thread. New session
           * created.
           *
           *
           * @param    sessionId
           *           The socket used as session id.
           * @param    fwdUser
           *           The FWD account.
           *
           * @return   See above.
           *
           * @throws   RestrictedUseException
           *           If the check for allowed caller fails.
           */
          public Object assignContext(Object sessionId, String fwdUser)
          throws RestrictedUseException
          {
             // restrict the use of this method
             SecurityUtil.checkCallerAbort(new String[] {
                "com.goldencode.p2j.main.WebDriverHandler.spawnWorker",
                "com.goldencode.p2j.security.SecurityManager.authenticateLocal"});

             if (!cfg.isServer() || fwdUser == null)
                return null;

             SecurityCache sc = getCache();
             SecurityContext newContext = contextSm.createSecurityContext(sc, null, fwdUser);

             if (newContext == null || sc != getCache())
             {
                if (LOG.isLoggable(Level.FINER))
                {
                   LOG.logp(Level.FINER,
                            SsoSecurityManager.class.getSimpleName(),
                            "assignContext",
                            "Newer security cache generation available; authentication aborted.");
                }
                return null;
             }

             // create new session
             sessionSm.addSession(sessionId, fwdUser, sc, newContext);
             contextSm.assignContext(newContext);
             SecurityContextStack.setContext(new SecurityContextStack(newContext));

             return newContext;
          }

          /**
           * Executes the client-side part of the SSO authentication type {@link SecurityConstants#AUTH_REQ_SSO}
           * and returns the AuthData.
           *
           * @param    socket
           *           The socket for the connection with the server.
           * @param    clientUuid
           *           The client uuid.
           * @param    fwdUser
           *           The fwd username.
           *
           * @return   The newly created security context for the authenticated FWD user.
           */
          private AuthData authenticateClientWorkerSso(NetSocket socket, String clientUuid, String fwdUser)
          {
             try
             {
                socket.writeInt(AUTH_REQ_SSO);
                socket.writeUTF(clientUuid + "," + fwdUser);
                socket.flush();

                if (socket.readInt() == AUTH_RESULT_SUCCESS)
                {
                   if (LOG.isLoggable(Level.FINER))
                   {
                      LOG.finer("Successful sso auth for fwd user " + fwdUser + " and client uuid " + clientUuid);
                   }
                   // only web drivers
                   return new AuthData(false, fwdUser, null);
                }
             }
             catch (IOException e)
             {
                LOG.warning("Failed sso auth for fwd user " + fwdUser + " and client uuid " + clientUuid, e);
                socket.close();
             }
             return null;
          }

          /**
           * Executes the server-side part of the SSO authentication type {@link SecurityConstants#AUTH_REQ_SSO}
           * and returns the newly created security context for the authenticated FWD user.
           *
           * @param    socket
           *           The socket for the connection with the client.
           * @param    sc
           *           The security cache.
           *
           * @return   The newly created security context for the authenticated FWD user.
           */
          private Object authenticateLocalSso(NetSocket socket, SecurityCache sc)
          {
             String clientUuid;
             String requestedUserName;
             try
             {
                String[] ssoDetails = socket.readUTF().split(",");
                clientUuid = ssoDetails[0];
                requestedUserName = ssoDetails[1];
             }
             catch (Exception e)
             {
                LOG.warning("Failed sso auth. Couldn't receive authentication details from the client.", e);
                return null;
             }

             if (requestedUserName.trim().equals(""))
                requestedUserName = null;

             Object ssoContext = null;

             boolean isFinerLoggable = LOG.isLoggable(Level.FINER);
             if (isFinerLoggable)
             {
                LOG.finer("Received SSO auth req for FWD user " + requestedUserName + " and client uuid " + clientUuid);
             }
             if (requestedUserName != null)
             {
                // we have that user in directory
                if (sc.getAccountById(requestedUserName) instanceof UserAccount)
                {
                   // it matches the client uuid
                   String fwdUser = WebDriverHandler.CLIENT_UUID_FWD_USERS.getOrDefault(clientUuid, null);
                   if (requestedUserName.equals(fwdUser))
                   {
                      try
                      {
                         // valid client uuid : fwd user received from client
                         ssoContext = ssoSm.assignContext(socket, fwdUser);
                         socket.writeInt(AUTH_RESULT_SUCCESS);
                         socket.flush();
                      }
                      catch (Exception e)
                      {
                         LOG.warning("Failed sso auth for fwd user " + fwdUser + " and client uuid " + clientUuid +
                                        ". Couldn't send to client the success result.", e);
                         return null;
                      }
                   }
                }
                else if (isFinerLoggable)
                {
                   LOG.finer("SSO authentication requested for fwd username " + requestedUserName +
                                 ", but account was not found.");
                }
             }

             if (isFinerLoggable)
             {
                LOG.finer((ssoContext != null ? "Successful" : "Failed") +
                              " authentication for SSO user " + requestedUserName +
                              " and client uuid " + clientUuid);
             }

             if (ssoContext == null)
             {
                try
                {
                   socket.writeInt(AUTH_RESULT_INVALID_USERID);
                   socket.writeInt(AUTH_ACTION_ABORT);
                   socket.flush();
                }
                catch (IOException e)
                {
                   LOG.warning("Couldn't send to client the sso authentication failure result.", e);
                }
             }
             return ssoContext;
          }
       }
    }