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