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