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