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