Audit.java

/*
** Module   : Audit.java
** Abstract : Implements security audit methods.
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -------------------------Description--------------------------------
** 001 NVS 20050329   @20527 Created. This class encapsulates security 
**                           audit implementation and exports logging 
**                           methods to be used for audit log records 
**                           creation. 
** 002 NVS 20050429   @20940 Added javadoc throughout the file. 
** 003 GES 20060305   @24894 Fixed security hole (made the logger 
**                           anonymous so that it could not be accessed
**                           and modified by untrusted code). Moved
**                           to a common logging approach.
** 004 ECF 20060614   @27244 Added isEnabled() method. Reports whether
**                           auditing is enabled.
** 005 NVS 20090821   @43713 Audit log file name can be customized to include
**                           the server name by inserting the "%s" token into
**                           the filename template. It becomes much more 
**                           convenient to share a single directory file 
**                           between multiple servers. 
**                           The audit record now includes both the account
**                           names and their ordinals.
** 006 ECF 20150715          Replace StringBuffer with StringBuilder.
** 007 TJD 20220504          Java 11 compatibility minor changes
** 008 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
*/ 
/*
** 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.*;
import java.util.logging.*;

/**
 * Provides controllable auditing capabilities for all security relevant
 * events.
 * <p>
 * Auditing is controllable by /security/audit branch of the P2J directory.
 * Various attributes of objects there specify the filtering of events and
 * where the log records go.
 *
 */
class Audit
{
   /** tells whether auditing is enabled or not */
   private boolean enabled = false;
   
   /** tells whether auditing is active or not */
   private boolean active = false;
   
   /** top level filters combination operation: OR (false) or AND (true) */
   private boolean filterMode = false;
   
   /** flags whether to audit successful accesses */
   private boolean auditSuccess = false;
   
   /** flags whether to audit failed accesses */
   private boolean auditFailure = false;
   
   /** set of numeric subject IDs to audit */
   private Set auditId = null;
   
   /** logger to pass records to */
   private Logger logger = null;

   /** handler that provides rotated file logs */
   private FileHandler handler = null;

   /** references to abstract resource type names */
   private String[] types = null;

   /** references to audit targets in registered plugins */
   private AuditTarget[] targets = null;

   /** log file pattern */
   private String logFile = null;

   /** log file size in K */
   private int logSize = 0;

   /** log file count in the rotation ring */
   private int logCount = 0;

   /**
    * A package private constructor that leaves auditing disabled.
    */
   Audit()
   {
   }

   /**
    * Package private constructor.
    * Must be called after the abstract resource plugins are loaded and
    * registered, so that the number of plugins is known and they have built
    * their audit targets.
    *
    * @param sCache
    *        instance of <code>SecurityCache</code> where this object belongs
    *
    * @param logFile
    *        pattern for audit log files to create
    *
    * @param logSize
    *        limit in Kbytes of each file
    *
    * @param logCount
    *        number of files to rotate
    * @throws IOException
    *        in <code>FileHandler</code>
    */
   Audit(SecurityCache sCache, String logFile, int logSize, int logCount)
   throws IOException
   {

      // remember log file parameters
      this.logFile  = logFile;
      this.logSize  = logSize;
      this.logCount = logCount;

      enabled = true;

      // build the array of audit targets for quick access
      int numRes = sCache.getNumResources();
      if (numRes <= 0)
         return;

      types = new String[numRes];
      targets = new AuditTarget[numRes];
      for (int i = 0; i < numRes; i ++)
      {
         types[i] = sCache.getRegistryByOrd(i).getTypeName();
         targets[i] = sCache.getRegistryByOrd(i).getTarget();
      }
   }

   /**
    * Activates audit log. 
    * Has no effect on disabled or already active log.
    *
    * @throws IOException
    *         if logging activation fails
    */
   synchronized void start()
   throws IOException
   {
      if (!enabled || active)
         return;

      // substitute the server name in the log file name
      StringBuilder sb = new StringBuilder();

      int pos = logFile.indexOf("%s");
      if ( pos >= 0)
      {
         if (pos > 0)
         {
            sb.append(logFile.substring(0, pos));
         }
         
         String name = SecurityManager.getInstance().getCache().
                       getServerAccount().getSubjectId(); 
         sb.append(name);
         sb.append(logFile.substring(pos + 2));
      }
      else
      {
         sb.append(logFile);
      }
      
      // initialize logging
      logger = Logger.getAnonymousLogger();
      logger.setUseParentHandlers(false);
      handler = new FileHandler(sb.toString(), logSize * 1024, logCount, true);
      logger.addHandler(handler);
      logger.setLevel(Level.INFO);

      active = true;
   }

   /**
    * Deactivates audit log.
    * Has no effect on disabled or inactive log.
    */
   synchronized void stop()
   {
      if (!enabled || !active)
         return;

      // deactivate logging
      handler.close();
      logger.removeHandler(handler);
      handler = null;

      active = false;
   }
   
   /**
    * Report whether auditing is enabled.
    *
    * @return  <code>true</code> if enabled, else <code>false</code>.
    */
   boolean isEnabled()
   {
      return enabled;
   }

   /**
    * Sets the top level filter combination operation.
    *
    * @param filterMode
    *        top level filters combination operation: OR (<code>false</code>)
    *        or AND (<code>true</code>)
    */
   void setFilterMode(boolean filterMode)
   {
      this.filterMode = filterMode;
   }

   /**
    * Tells whether to audit successful accesses. 
    *
    * @param auditSuccess
    *        if <code>true</code>, tells to audit successful accesses
    */
   void setAuditSuccess(boolean auditSuccess)
   {
      this.auditSuccess = auditSuccess;
   }

   /**
    * Tells whether to audit failed accesses. 
    *
    * @param auditFailure
    *        if <code>true</code>, tells to audit failed accesses
    */
   void setAuditFailure(boolean auditFailure)
   {
      this.auditFailure = auditFailure;
   }

   /**
    * Enables the numeric subject IDs for auditing. 
    * New set of IDs completely replaces the old one.
    *
    * @param auditId
    *        array of IDs to enable for auditing
    */
   void setAuditId(int[] auditId)
   {
      if (auditId == null)
         return;

      this.auditId = new HashSet();

      for (int i = 0; i < auditId.length; i ++)
      {
         this.auditId.add(Integer.valueOf(auditId[i]));
      }
   }

   /**
    * Creates an audit log record.
    * <p>
    * Log records are text based, with multiple comma-separated values.
    * The order of values is fixed and listed below:
    * <ul>
    *   <li>security context - from 0 to 3 colon separated numbers;
    *   <li>resource type name;
    *   <li>resource instance name;
    *   <li>requested access mode for this instance of the resource;
    *   <li>decision made about this access.
    * </ul>
    *
    * @param resType
    *        numeric abstract resource type (registration index)
    *
    * @param instance
    *        resource instance name
    *
    * @param mode
    *        resource instance access mode (requested rights)
    *
    * @param result
    *        granted (<code>true</code>) or denied (<code>false</code>)
    */
   void log(int resType, String instance, int mode, boolean result)
   {
      log(resType, instance, mode, result, null);
   }

   /**
    * Creates an audit log record.
    * <p>
    * Log records are text based, with multiple comma-separated values.
    * The order of values is fixed and listed below:
    * <ul>
    *   <li>security context - from 0 to 3 colon separated account name[number] 
    *       pairs;
    *   <li>resource type name;
    *   <li>resource instance name;
    *   <li>requested access mode for this instance of the resource;
    *   <li>decision made about this access;
    *   <li>additional message - an arbitrary string of text. 
    * </ul>
    *
    * @param resType
    *        numeric abstract resource type (registration index)
    * @param instance
    *        resource instance name
    * @param mode
    *        resource instance access mode (requested rights)
    * @param message
    *        Arbitrary text to be included into the record.  May be
    *        <code>null</code>
    * @param result
    *        granted (<code>true</code>) or denied (<code>false</code>)
    */
   void log(int     resType,
            String  instance,
            int     mode,
            boolean result,
            String  message)
   {
      // verify that auditing is enabled
      if (!enabled)
         return;

      // verify that this record is desirable
      if (!filter(resType, instance, mode, result))
          return;

      // OK, now do logging
      StringBuilder sb = new StringBuilder();

      SecurityContextStack ctx = SecurityContextStack.getContext();
      if (ctx != null)
      {
         SecurityCache sc = ctx.getCache();
         int[] checkList = ctx.getCheckList();
         
         for (int i = 0; i < checkList.length; i ++)
         {
            sb.append(sc.getAccountByOrd(checkList[i]).getSubjectId());
            sb.append("[");
            sb.append(checkList[i]);
            sb.append("]:");
         }
         sb.setLength(sb.length() - 1);
      }
      sb.append(",");

      sb.append(types[resType]);
      sb.append(",");
      sb.append(instance);
      sb.append(",");
      sb.append(mode);
      sb.append(",");
      sb.append(result);
      
      if (message != null)
      {
         sb.append(",");
         sb.append(message);
      }

      // synchronization below is required between this block, start(), stop()
      // methods and a block in SecurityCacheRefresh (SecurityManager) who
      // swaps caches, so that there is no interruption in audit logging

      synchronized (this)
      {
         logger.info(new String(sb));
      }
   }

   /**
    * Filters out audit targets.
    * <p> Uses numeric resource type index to get to the appropriate audit
    * target. The decision is made based on the independent checks (listed
    * below) and the combination operation, OR or AND.
    * <p>These checks are done independently:
    * <ul>
    *   <li>the result parameter matches either auditSuccess or auditFailure;
    *   <li>the security context has numeric account ID matching one of those
    *       listed in auditId set;
    *   <li>the resource instance and access mode constitute a valid audit
    *       target
    * </ul>
    * If the combination operation is OR (filterMode value is false), than the
    * final decision is <code>true</code> if any of the checks yields 
    * <code>true</code>, otherwise, all checks should yield <code>true</code>.
    *
    * @param resType
    *        numeric abstract resource type (registration index)
    *
    * @param instance
    *        resource instance name
    *
    * @param mode
    *        resource instance access mode (requested rights)
    *
    * @param result
    *        granted (<code>true</code>) or denied (<code>false</code>)
    *
    * @return <code>true</code> if this combination represents a valid audit
    *        target
    */
   private boolean filter(int resType, String instance, int mode, 
                          boolean result)
   {
      if (filterMode)
      {
         // all checks have to yield true
         // check 1: decision
         if (result && !auditSuccess)
            return false;
         if (!result && !auditFailure)
            return false;

         // check 2: subject IDs
         if (auditId != null && !auditId.isEmpty())
         {
            SecurityContextStack ctx = SecurityContextStack.getContext();
            if (ctx != null)
            {
               int[] checkList = ctx.getCheckList();
               int i = 0;
               for (i = 0; i < checkList.length; i ++)
               {
                  if (auditId.contains(Integer.valueOf(checkList[i])))
                     break;
               }
               if (i == checkList.length)
                  return false;
            }
         }
      }
      else
      {
         // any check has to yield true
         // check 1: decision
         if (result && auditSuccess)
            return true;
         if (!result && auditFailure)
            return true;

         // check 2: subject IDs
         if (auditId != null && !auditId.isEmpty())
         {
            SecurityContextStack ctx = SecurityContextStack.getContext();
            if (ctx != null)
            {
               int[] checkList = ctx.getCheckList();
               for (int i = 0; i < checkList.length; i ++)
               {
                  if (auditId.contains(Integer.valueOf(checkList[i])))
                     return true;
               }
            }
         }
      }

      // check 3: resource target
      return targets[resType].checkTarget(instance, mode);
   }
}