SecurityUtil.java

/*
 ** Module   : SecurityUtil.java
 ** Abstract : Wrapper class for security util methods (moved from SecurityManager).
 **
 ** Copyright (c) 2023-2025, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ------------------------------------Description-------------------------------------------
 ** 001 GBB  20230825 Initial version.
 ** 002 CA   20231018 Added LegacyWebSecurityManager as an internal class.
 ** 003 GBB  20230825 Added MultiSessionAppserverSecurityManager as an internal class.
 */
/*
 ** 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.util.*;

/** Wrapper class for security util methods */
public class SecurityUtil
{
   
   /** A list of all security classes that can forward a check call and should be ignored. */
   private static final Set<String> INTERNAL_CLASS_NAMES = new HashSet<String>() {{
      add(SecurityUtil.class.getName());
      add(SecurityManager.class.getName());
      add(SecurityManager.SessionSecurityManager.class.getName());
      add(SecurityManager.ContextSecurityManager.class.getName());
      add(SecurityManager.VirtualConnectionSecurityManager.class.getName());
      add(SecurityManager.SsoSecurityManager.class.getName());
      add(MultiSessionAppserverSecurityManager.class.getName());
      add(LegacyWebSecurityManager.class.getName());
   }};

   /**
    * Package a user ID and password into a single entity for transmission.
    *
    * @param    userid
    *           The subject ID.
    * @param    password
    *           The password.
    *
    * @return   The object containing the packaged values.
    */
   public static byte[] packageIdPassword(String userid, String password)
   {
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      DataOutputStream      dos = new DataOutputStream(bos);
      
      try
      {
         dos.writeUTF(userid);
         dos.writeUTF(password);
      }
      
      catch (IOException ioe)
      {
         return new byte[0];
      }
      
      return bos.toByteArray();      
   }

   /**
    * Checks the call stack to see if the caller is any of the methods
    * specified.
    * <p>
    * The closest entry to the top of the stack where the class name differs 
    * from both the <code>SecurityManager</code> and the given
    * <code>ignore</code> class name is the entry checked.
    * <p>
    * A function of <code>SecurityManager</code> can be specified as the
    * allowed function. In this case it should be the closest entry to the
    * top except <code>ignore</code> class functions and other functions of
    * <code>SecurityManager</code>.
    * <p>
    * This arrangement allows for multiple levels of calls within this class
    * and the class which is checking the caller without affecting the check.
    *
    * @param    allowed
    *           A list of fully qualified (package.class.method) names from
    *           which the call is allowed.
    * @param    ignore
    *           A class name of entries (other than that of the
    *           <code>SecurityManager</code>) which should be ignored at
    *           the top of the stack.  May be <code>null</code>.
    *
    * @throws   RestrictedUseException
    *           If the check fails.
    */
   public static void checkCallerAbort(String[] allowed, String ignore)
   throws RestrictedUseException
   {
      if (!checkCallerWorker(allowed, null, ignore))
      {
         throw new RestrictedUseException("Unauthorized caller!");
      }
   }

   /**
    * Checks the call stack to see if the caller is any of the methods
    * specified.
    * <p>
    * The closest entry to the top of the stack where the class name differs 
    * from the <code>SecurityManager</code> is the entry checked.
    * <p>
    * This arrangement allows for multiple levels of calls within this class
    * and the class which is checking the caller without affecting the check.
    *
    * @param    allowed
    *           A list of fully qualified (package.class.method) names from
    *           which the call is allowed.
    *
    * @throws   RestrictedUseException
    *           If the check fails.
    */
   public static void checkCallerAbort(String[] allowed)
   throws RestrictedUseException
   {
      checkCallerAbort(allowed, null);
   }

   /**
    * Checks the call stack to see if the caller is the method specified.
    * <p>
    * The closest entry to the top of the stack where the class name differs 
    * from both the <code>SecurityManager</code> and the given
    * <code>ignore</code> class name is the entry checked.
    * <p>
    * A function of <code>SecurityManager</code> can be specified as the
    * allowed function. In this case it should be the closest entry to the
    * top except <code>ignore</code> class functions and other functions of
    * <code>SecurityManager</code>.
    * <p>
    * This arrangement allows for multiple levels of calls within this class
    * and the class which is checking the caller without affecting the check.
    *
    * @param    allowed
    *           A fully qualified (package.class.method) name from which the
    *           call is allowed.
    * @param    ignore
    *           A class name of entries (other than that of the
    *           <code>SecurityManager</code>) which should be ignored at
    *           the top of the stack.  May be <code>null</code>.
    *
    * @throws   RestrictedUseException
    *           If the check fails.
    */
   public static void checkCallerAbort(String allowed, String ignore)
   throws RestrictedUseException
   {
      StringBuilder sb = new StringBuilder();
      
      if (!checkCallerWorker(new String[] {allowed}, sb, ignore))
      {
         throw new RestrictedUseException("This restricted use method " +
                                          "cannot be called from " +
                                          sb.toString());
      }
   }

   /**
    * Checks the call stack to see if the caller is the method specified.
    * <p>
    * The closest entry to the top of the stack where the class name differs 
    * from the <code>SecurityManager</code> is the entry checked.
    * <p>
    * This arrangement allows for multiple levels of calls within this class
    * and the class which is checking the caller without affecting the check.
    *
    * @param    allowed
    *           A fully qualified (package.class.method) name from which the
    *           call is allowed.
    *
    * @throws   RestrictedUseException
    *           If the check fails.
    */
   public static void checkCallerAbort(String allowed)
   throws RestrictedUseException
   {
      checkCallerAbort(allowed, null);
   }

   /**
    * Checks the call stack to see if the caller is the method specified.
    * <p>
    * The closest entry to the top of the stack where the class name differs 
    * from both the <code>SecurityManager</code> and the given
    * <code>ignore</code> class name is the entry checked.
    * <p>
    * A function of <code>SecurityManager</code> can be specified as the
    * allowed function. In this case it should be the closest entry to the
    * top except <code>ignore</code> class functions and other functions of
    * <code>SecurityManager</code>.
    * <p>
    * This arrangement allows for multiple levels of calls within this class
    * and the class which is checking the caller without affecting the check.
    *
    * @param    allowed
    *           A fully qualified (package.class.method) name from which the
    *           call is allowed.
    * @param    ignore
    *           A class name of entries (other than that of the
    *           <code>SecurityManager</code>) which should be ignored at
    *           the top of the stack.  May be <code>null</code>.
    *
    * @return   <code>true</code> if allowed.
    */
   public static boolean checkCaller(String allowed, String ignore)
   {
      return checkCallerWorker(new String[] {allowed}, null, ignore);
   }

   /**
    * Checks the call stack to see if the caller is the method specified.
    * <p>
    * The closest entry to the top of the stack where the class name differs 
    * from both the <code>SecurityManager</code> is the entry checked.
    * <p>
    * This arrangement allows for multiple levels of calls within this class
    * and the class which is checking the caller without affecting the check.
    *
    * @param    allowed
    *           A fully qualified (package.class.method) name from which the
    *           call is allowed.
    *
    * @return   <code>true</code> if allowed.
    */
   public static boolean checkCaller(String allowed)
   {
      return checkCaller(allowed, null);
   }

   /**
    * Checks the call stack to see if the caller is the method specified.
    * <p>
    * The closest entry to the top of the stack where the class name differs 
    * from both the <code>SecurityManager</code> and the given
    * <code>ignore</code> class name is the entry checked.
    * <p>
    * A function of <code>SecurityManager</code> can be specified as the
    * allowed function. In this case it should be the closest entry to the
    * top except <code>ignore</code> class functions and other functions of
    * <code>SecurityManager</code>.
    * <p>
    * This arrangement allows for multiple levels of calls within this class
    * and the class which is checking the caller without affecting the check.
    *
    * @param    allowed
    *           A list of fully qualified (package.class.method) names from
    *           which the call is allowed.
    * @param    entry
    *           Optional user-supplied buffer so that on failure the invalid
    *           value will be available.  May be <code>null</code> if not
    *           needed by the caller.
    * @param    ignore
    *           A class name of entries (other than that of the
    *           <code>SecurityManager</code>) which should be ignored at
    *           the top of the stack.  May be <code>null</code>.
    *
    * @return   <code>true</code> if allowed.
    */
   private static boolean checkCallerWorker(String[] allowed, StringBuilder entry, String ignore)
   {
      if (allowed == null)
         return false;
      
      if (entry == null)
         entry = new StringBuilder();
      
      // obtain the stack trace (expensive!)
      Throwable thread = new Throwable();
      StackTraceElement[] stack = thread.getStackTrace();
      
      // the following loop starts at the 1 index position because the
      // 0 index pos will always be the current method... which doesn't
      // need checking <g>
      
      for (int i = 1; i < stack.length; i++)
      {
         String possibleClass = stack[i].getClassName();

         // check ignore class
         if (ignore != null && possibleClass.equals(ignore))
            continue;

         entry.setLength(0);
         entry.append(possibleClass).append(".");
         entry.append(stack[i].getMethodName());
         String possibleMethod = entry.toString();

         for (int j = 0; j < allowed.length; j++)
         {
            if (possibleMethod.equals(allowed[j]))
            {
              return true;
            }
         }

         if (INTERNAL_CLASS_NAMES.contains(possibleClass))
         {
            // avoid internal calls
            continue;
         }

         // no match
         break;
      }

      return false;
   }

   /**
    * Creates a valid filename using a fully qualified class name.
    *
    * @param    name
    *           A fully qualified class name (without the ".class" at the
    *           end). Note that any "." characters are converted into the
    *           system-specific file separators.
    *
    * @return   The filename.
    */
   static String buildClassFileName(String name)
   {
      // this is preparation to get the file from jar; always use "/", this is correct even on
      // Windows
      String path = name.replaceAll("\\.", "/");
      String sb   = path + ".class";
      
      return sb;
   }

   /**
    * Checks the validity (existence) of a class, first creating a filename
    * using a given fully qualified class name.
    *
    * @param    name
    *           A fully qualified class name (without the ".class" at the
    *           end). Note that any "." characters are converted into path
    *           separators.
    *
    * @return   <code>true</code> if the class file exists.
    */
   static boolean classExists(String name)
   {
      String path = buildClassFileName(name);
      return (ClassLoader.getSystemResource(path) != null);
   }

   /**
    * Gets a byte array filled with the binary class data for the given
    * class.
    *
    * @param    name
    *           A fully qualified class name (without the ".class" at the
    *           end).
    *
    * @return   The array of bytes of the binary class file image.
    */
   static byte[] getClassBytes(String name)
   {
      // compose resource name
      String path = buildClassFileName(name);
      InputStream in = ClassLoader.getSystemResourceAsStream(path);
      
      if (in == null)
         return null;

      // create the output stream for byte array
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      final int BUFFSIZE = 8192;
      byte[] buff = new byte[BUFFSIZE];
      int count = 0;

      // read the class file manually
      try
      {
         do
         {
            count = in.read(buff);
            if (count > 0)
               baos.write(buff, 0, count);
         } while (count > 0);
      }
      catch (IOException exc)
      {
         return null;
      }
      finally
      {
         try
         {
            in.close();
         }
         catch (IOException exc)
         {
         }
      }
      
      // return the class data
      return baos.toByteArray();
   }
}