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