SecurityContext.java
/*
** Module : SecurityContext.java
** Abstract : manages security context for server threads
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------------Description-----------------------------------
** 001 NVS 20050307 @20216 Created. This class manages security contexts
** 002 NVS 20050427 @20911 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.
** 003 NVS 20050504 @21067 unassign() method is now responsible for the
** security context cleanup, which is due when
** the use counter reaches 0. Added a private
** method called cleanup() where this processing
** takes place.
** 004 NVS 20050506 @21116 Security contexts are now bound to generation
** of security cache they are built on. This
** change help in fixing the security cache
** refresh bug. Added private member to keep the
** reference to the cache and getCache() method.
** 005 ECF 20060120 @24004 Modified {add|remove|has|get}Token methods.
** Replaced String token name with an Object
** key.
** 006 ECF 20060123 @24035 Added read-only session ID. The unique ID is
** set by the SecurityManager at SecurityContext
** construction. One session ID is reserved for
** the server/system context.
** 007 NVS 20070412 @32965 Reworked security context cleanup. cleanup()
** method is made package private. It isn't
** called from unassign() now. Instead, unassign
** returns a boolean indicating the state of
** the context.
** 008 ECF 20070708 @34418 Optimized getToken() and removeToken().
** Removed unnecessary checks preceding actual
** retrieval/removal.
** 009 NVS 20080513 @38273 Fixed javadoc for unassign().
** 010 ECF 20081006 @40040 Added safety logic and logging to cleanup().
** Cleanable's cleanup() calls are now enclosed
** in a try-catch block, with logging on error.
** 011 CA 20091119 @44431 Added SessionToken implementation - an unique
** object is kept for each security context, which
** will hold the session ID and the subject.
** 012 GES 20111021 Added the transferAllTokens() method.
** 013 CA 20120727 The cleanup() method should check if the payload
** is an instance of Cleanable - testing if one of
** its directly implemented interfaces is Cleanable
** poses a risk of not identifying a payload which
** does not directly implement Cleanable, but its
** superclass (or someone else up the hierarchy)
** does.
** Also, the tokenMap is cleared at the beginning of
** cleanup(), to ensure no other thread can start
** the cleaning process again.
** 014 CA 20130628 Added possibility to reset a context (needed by appservers running in
** State-reset operating mode).
** 015 CA 20140413 Fix for H014 - ensure the context-local vars are cleaned up in a
** predetermined order, when the security context is reset.
** Context cleanup uses same weights as context-reset, except all vars
** are cleaned, unconditionally.
** 016 IAS 20160407 Refactoring and optimization.
** 017 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.
** 018 GES 20200205 Provide a mechanism to create a copy that does not share any mutable
** state. This is used to create a context that is related to an
** existing context without sharing the same session.
** 019 SVL 20200304 Javadoc fix.
** 020 GES 20210917 Rework to better encapsulate decision caching.
** 021 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 022 CA 20231026 Manually remove the HEADLESS_KEY from then token map when the context is cleaned.
** 023 GBB 20250403 Lower logging level of SecurityContext.cleanupWorker log to WARNING.
*/
/*
** 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 com.goldencode.p2j.util.logging.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import java.util.logging.*;
import java.util.stream.*;
/**
* Security context is a collection of control data that associates subjects
* with Access Control Lists in the directory.
*/
class SecurityContext
{
/** Logger. */
private static final CentralLogger LOG = CentralLogger.get(SecurityContext.class);
/** Reserved session ID for system threads */
static final int SYSTEM_SESSION = 0;
/** flags the initial security context */
private final boolean isInitial;
/** index of process account */
private final int procAcc;
/** index of user account */
private final int userAcc;
/** indices of group accounts */
private final int[] grpsAcc;
/** combined rights check list */
private final int[] accList;
/** use count for this context */
private final AtomicInteger use = new AtomicInteger(0);
/** Cache of access decisions made under this context. */
private final Decision[] decisionCache;
/** saved procId */
private final int procId;
/** saved userId */
private final int userId;
/** saved groupIds */
private final int[] groupId;
/** a map of arbitrary tokens that are associated with the context */
private final ConcurrentMap<ContextKey, Object> tokenMap = new ConcurrentHashMap<>(257);
/** security cache this context stack is bound to */
private final SecurityCache cache;
/** unique session token for this security context */
private final SessionToken sessionToken;
/** Lock to avoid the cleanup to take place simultaneously. */
private Object cleanupLock = new Object();
/**
* Package private constructor used to create a user or system session
* context.
*
* @param cache
* security cache to bind this context to
* @param proc
* index of the process account or -1
* @param user
* index of the user account or -1
* @param groups
* array of indices of the group accounts or null
* @param nres
* number of registered resources
* @param session
* Unique session ID for this context
* @param subject
* Subject authenticated in this context. This can only be
* <code>null</code> when the <code>session</code> is set to the
* {@link #SYSTEM_SESSION}.
*
* @throws NullPointerException
* if the <code>subject</code> is null and this is not a system
* session.
*/
SecurityContext(SecurityCache cache, int proc, int user, int[] groups,
int nres, int session, String subject)
{
this(cache, proc, user, groups, nres, session, subject, false);
}
/**
* Package private constructor used to create a system session context.
*
* @param cache
* security cache to bind this context to
* @param proc
* index of the process account or -1
* @param user
* index of the user account or -1
* @param groups
* array of indices of the group accounts or null
* @param nres
* number of registered resources
* @param initial
* flag for initial security context
*/
SecurityContext(SecurityCache cache, int proc, int user, int[] groups,
int nres, boolean initial)
{
this(cache, proc, user, groups, nres, SYSTEM_SESSION, null, initial);
}
/**
* Create a copy of the instance, which represents a different session but otherwise shares
* immutable state.
*
* @param other
* The instance on which to pattern the new copy.
*/
SecurityContext(SecurityContext other)
{
this(other.cache,
other.procAcc,
other.userAcc,
other.grpsAcc,
other.decisionCache.length,
other.sessionToken.getSessionID(), // this must be overridden later
other.sessionToken.getSubject(),
other.isInitial);
}
/**
* Private constructor used to create a user or system session
* context.
*
* @param cache
* security cache to bind this context to
* @param proc
* index of the process account or -1
* @param user
* index of the user account or -1
* @param groups
* array of indices of the group accounts or null
* @param nres
* number of registered resources
* @param session
* Unique session ID for this context
* @param subject
* Subject authenticated in this context. This can only be
* <code>null</code> when the <code>session</code> is set to the
* {@link #SYSTEM_SESSION}.
* @param initial
* flag for initial security context
*
* @throws NullPointerException
* if the <code>subject</code> is null and this is not a system
* session.
*/
private SecurityContext(SecurityCache cache, int proc, int user, int[] groups,
int nres, int session, String subject, boolean initial)
{
if (session != SYSTEM_SESSION && subject == null)
{
throw new NullPointerException("Creating a security context with " +
"a null session token is not allowed !");
}
this.cache = cache;
int len = groups != null ? groups.length : 0;
procAcc = proc;
userAcc = user;
grpsAcc = groups;
sessionToken = new SessionToken(session, subject);
accList = new int[len + 1];
if (user != -1)
accList[0] = user;
else
accList[0] = proc;
for (int i = 0; i < len; i ++)
accList[i + 1] = groups[i];
decisionCache = new Decision[nres];
for (int i = 0; i < nres; i ++)
{
decisionCache[i] = new Decision();
}
procId = proc;
userId = user;
groupId = groups;
isInitial = initial;
}
/**
* Set the context cleanup {@link #cleanupLock lock}, to avoid concurrent cleanups.
*
* @param lock
* The instance on which to lock for context cleanup.
*/
void setCleanupLock(Object lock)
{
this.cleanupLock = lock;
}
/**
* Get the context cleanup {@link #cleanupLock lock}, to avoid concurrent cleanups.
*
* @return The instance on which to lock for context cleanup.
*/
Object getCleanupLock()
{
return cleanupLock;
}
/**
* Marks this security context as assigned by incrementing its use count.
*/
void assign()
{
use.incrementAndGet();
}
/**
* Marks this security context as unassigned by decrementing its use count.
*
* @return <code>true</code> if the use count has reached 0, otherwise
* <code>false</code>
*/
boolean unassign()
{
return use.decrementAndGet() == 0;
}
/**
* Gets the array of accounts associated with this context.
*
* @return array of indices into accounts
*/
int[] getCheckList()
{
return accList;
}
/**
* Gets the array of original subject IDs of this context.
*
* @return array of indices into accounts
*/
int[] getIdList()
{
int groups = groupId == null ? 0 : groupId.length;
int[] list = new int[2 + groups];
list[0] = procId;
list[1] = userId;
for (int i = 0; i < groups; i ++)
list[2 + i] = groupId[i];
return list;
}
/**
* Checks whether this context is initial.
*
* @return <code>true</code> if this context is initial
*/
boolean isInitial()
{
return isInitial;
}
/**
* Gets process account index.
*
* @return process account index
*/
int getProcessOrdinal()
{
return procAcc;
}
/**
* Gets user account index.
*
* @return user account index
*/
int getUserOrdinal()
{
return userAcc;
}
/**
* Gets session ID.
*
* @return session ID.
*/
int getSessionId()
{
return sessionToken.getSessionID();
}
/**
* This is used to change the session id at a point following construction.
*
* @param id
* The new session id.
*/
void morphSessionId(int id)
{
sessionToken.setSessionID(id);
}
/**
* Gets session token.
*
* @return session token.
*/
SessionToken getSessionToken()
{
return sessionToken;
}
/**
* Get the access decision cache.
*
* @param resource
* The unique index that identifies the resource type.
*
* @return The decision cache.
*/
Decision getDecisionCache(int resource)
{
return decisionCache[resource];
}
/**
* Gets the security cache this context is bound to.
*
* @return <code>SecurityCache</code>
*/
SecurityCache getCache()
{
return cache;
}
/**
* 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)
{
return tokenMap.putIfAbsent(key, token) == null;
}
/**
* 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)
{
return (tokenMap.remove(key) != null);
}
/**
* 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
*/
boolean hasToken(ContextKey key)
{
return tokenMap.containsKey(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>
*/
Object getToken(ContextKey key)
{
return tokenMap.get(key);
}
/**
* Copies all tokens to the given context and clears the token map of the
* current instance.
*/
void transferAllTokens(SecurityContext other)
{
// ConcurrentMap iterator allows concurrent modifications
tokenMap.keySet().stream().forEach(key ->
{
Object val = tokenMap.remove(key);
if (val != null)
{
other.tokenMap.put(key, val);
}
});
}
/**
* Reset this security context.
*/
void reset()
{
// avoid cleanup to be called multiple times
synchronized (cleanupLock)
{
cleanupWorker(true);
}
}
/**
* Finalizes this security context.
*/
void cleanup()
{
// avoid cleanup to be called multiple times
synchronized (cleanupLock)
{
cleanupWorker(false);
}
}
/**
* Actual worker which fully cleans or just resets the context-local data.
*
* @param reset
* When {@code true}, this flag resets the security context; else, full cleanup is
* performed.
*/
private void cleanupWorker(boolean reset)
{
// for debugging purposes only
final boolean debug = false;
if (debug)
{
ContextLocal.setDebug(true);
}
// using a cloned map because we might clear the tokenMap
Map<ContextKey, Object> clone = new HashMap<>(tokenMap);
Set<ContextKey> removed = cleanupWorker(reset, clone);
// for debugging purposes only
if (debug)
{
ContextLocal.dumpDependencies();
}
if (!reset)
{
// manually remove the HEADLESS_KEY
tokenMap.remove(ContextKey.HEADLESS_KEY);
}
// Check that all keys which had to be removed where indeed removed
List<String> failedTokens = tokenMap.keySet().stream().filter(removed::contains)
.map(key -> key.getClass().getName())
.collect(Collectors.toList());
if (!failedTokens.isEmpty())
{
SecurityManager secMgr = SecurityManager.getInstance();
if (secMgr != null)
{
String msg = "SecurityContext.cleanupWorker did not complete properly - " +
"the following tokens are still in use: %s";
LOG.log(Level.WARNING, msg, failedTokens.toString());
}
}
// even if reset was not successful, ensure only the expected tokens
// will survive the reset
tokenMap.keySet().removeAll(removed);
}
/**
* Actual worker which fully cleans or just resets the copy of the context-local data.
*
* @param reset
* When {@code true}, this flag resets the security context; else, full cleanup is
* performed.
* @param tm
* the tokenMap copy
*
* @return the set of keys which where removed from the tm
* (and supposed to be removed from the tokenMap).
*/
private static Set<ContextKey> cleanupWorker(boolean reset, Map<ContextKey, Object> tm)
{
List<ContextKey> allKeys = new ArrayList<>();
List<WeightedToken> weightedKeys = new ArrayList<>();
Set<ContextKey> removed = new HashSet<>();
Predicate<ContextKey> doReset = key -> !reset ||
!(key instanceof ContextLocal) ||
((ContextLocal) key).isResetAllowed();
// collect all weighted tokens and sort them
tm.keySet().stream().filter(doReset).forEach(key ->
{
if (key instanceof WeightedToken)
{
weightedKeys.add((WeightedToken) key);
}
else
{
allKeys.add(key);
}
});
Collections.sort(weightedKeys, (o1, o2) -> o1.getWeight().compareTo(o2.getWeight()));
// add all the weighted keys last
weightedKeys.stream().forEach(key -> allKeys.add((ContextKey) key));
// get every token from the context
for (ContextKey key: allKeys)
{
Object payload = tm.get(key);
if (payload != null)
{
reset(payload);
removed.add(key);
}
}
return removed;
}
/**
* Reset the object if applicable
*
* @param payload
* the object to reset
*/
private static void reset(Object payload)
{
// check whether this token implements Cleanable interface
if (payload instanceof Cleanable)
{
try
{
((Cleanable) payload).cleanup();
}
catch (Exception e)
{
SecurityManager secMgr = SecurityManager.getInstance();
if (secMgr != null)
{
LOG.severe("Error cleaning up security context", e);
}
}
}
}
}