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