All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nhindirect.common.audit.impl.FileAuditor Maven / Gradle / Ivy

There is a newer version: 8.0.0
Show newest version
/* 
Copyright (c) 2010, NHIN Direct Project
All rights reserved.

Authors:
   Greg Meyer      [email protected]
 
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 
in the documentation and/or other materials provided with the distribution.  Neither the name of the The NHIN Direct Project (nhindirect.org). 
nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 
THE POSSIBILITY OF SUCH DAMAGE.
*/

package org.nhindirect.common.audit.impl;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.management.ManagementFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Locale;
import java.util.UUID;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;

import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.StandardMBean;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nhindirect.common.audit.AuditContext;
import org.nhindirect.common.audit.AuditEvent;
import org.nhindirect.common.audit.AuditorMBean;

import javax.management.openmbean.ArrayType;

/**
 * File based auditor.  Events are stored in a non-circular flat file that is not truncated.  Each event is appended to the end of the audit file.
 * @author Greg Meyer
 * @since 1.0
 */
public class FileAuditor extends AbstractAuditor implements AuditorMBean
{		
	@SuppressWarnings("deprecation")
	private static final Log LOGGER = LogFactory.getFactory().getInstance(FileAuditor.class);	
	
	/* record meta data goes at the end of each record */
	private static final short RECORD_METADATA_SIZE = 36; 
	
	private static final int RECORD_META_WRAPPER = 0xFFFFFFFF;
	
	private static final DateFormat df = DateFormat.getDateInstance(DateFormat.LONG, Locale.getDefault());

	
	private static final String EVENT_ID = "EVENT ID";
	private static final String EVENT_PRINCIPAL = "EVENT PRINCIPAL";	
	private static final String EVENT_TIME = "EVENT TIME";	
	private static final String EVENT_NAME = "EVENT CATEGORY";
	private static final String EVENT_TYPE = "EVENT MESSAGE";
	private static final String EVENT_CTX = "EVENT CONTEXTS";	
	
	private static final String EVENT_TAG_DELIMITER = "@@@@\r\n";	
	private static final String CONTEXT_TAG_DELIMITER = "====\r\n";
	
	private final RandomAccessFile auditFile;
	private AtomicInteger recordCount = new AtomicInteger();
	
	private CompositeType eventType;
	private String[] itemNames;
	
	
	/**
	 * Constructor.  If the audit file does not exist, then a new file is created barring access permissions or illegal file names or locations.  If the file already
	 * existing, then the file opened in append mode and new events are written to the end of the file.
	 * @param auditFile File descriptor of the audit file.
	 */
	public FileAuditor(File auditFile)
	{
		if (auditFile == null)
			throw new IllegalArgumentException("Audit file cannot be null.");
		
		LOGGER.info("Instantiating FileAuditor");
		
		if (!auditFile.exists())
		{
			LOGGER.info("Audit file does not exist.  Creating new file " + auditFile.getAbsolutePath());
			try
			{
				if (!auditFile.createNewFile())
					throw new IllegalArgumentException("Audit file could not be created.");
			}
			catch (IOException e)
			{
				throw new IllegalArgumentException("Audit file could not be created.", e);
			}
		}
		else
			LOGGER.info("Found existing audit file " + auditFile.getAbsolutePath() + "   Opening in read/write mode.");
		
		try
		{
			this.auditFile = new RandomAccessFile(auditFile, "rw");
		}
		catch (FileNotFoundException e)
		{
			throw new IllegalArgumentException("Audit file could not be found or created.", e);
		}
		
		// initialize the auditor state and validate that the file is not corrupt
		initAuditor();
		
		// register the auditor as an MBean
		registerMBean();
	}
	
	/*
	 * Initialize the auditor.  This includes consistency checks for corruption
	 */
	private void initAuditor()
	{
		try
		{
			if (auditFile.length() == 0)
			{
				// new file
				recordCount.set(0);
				auditFile.seek(0);			
			}
			else
			{
				// existing file
				// check it to make sure it is not corrupt
								
				// set the current file position to the last valid record				
				long currentPosition = auditFile.length();
				
				// start at the end of the file and work backwards
				boolean foundValidRecord = false;
				boolean needsFixing = false;
				while (currentPosition >= RECORD_METADATA_SIZE)
				{
					foundValidRecord = isCurrentRecordValid(currentPosition);
					if (foundValidRecord)
						break;							
					else if (!needsFixing)
					{
						LOGGER.warn("Inconsistencies found in audit file.  Attempting to fix issues.  Some data may be lost."); 
						needsFixing = true;
					}
					
					--currentPosition;
				}
				
				// if we could not find a valid record, then just start over
				if (!foundValidRecord)
				{
					recordCount.set(0);
					auditFile.seek(0);
				}
			}
		}
		catch (IOException e)
		{
			throw new IllegalStateException("Audit file is corrupt or could not be read.", e);
		}
	}
	
	/*
	 * Register the MBean
	 */
	private void registerMBean()
	{
		
		LOGGER.info("Registering FileAuditor MBean");
		
		try
		{
			itemNames = new String[] {"Event Id", "Event Time", "Event Principal", "Event Name", "Event Type", "Contexts"};
			
			OpenType[] types = {SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, 
					SimpleType.STRING, ArrayType.getArrayType(SimpleType.STRING)};
			
			eventType = new CompositeType("AuditEvent", "Direct Auditable Event", itemNames, itemNames, types);
		}
		catch (OpenDataException e)
		{
			LOGGER.error("Failed to create settings composite type: " + e.getLocalizedMessage(), e);
			return;
		}
		
		Class clazz = this.getClass();
		final StringBuilder objectNameBuilder = new StringBuilder(clazz.getPackage().getName());
		objectNameBuilder.append(":type=").append(clazz.getSimpleName());
		objectNameBuilder.append(",name=").append(UUID.randomUUID());
				
		try
		{			
			final StandardMBean mbean = new StandardMBean(this, AuditorMBean.class);
		
			final MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
			mbeanServer.registerMBean(mbean, new ObjectName(objectNameBuilder.toString()));
		}
		catch (JMException e)
		{
			LOGGER.error("Unable to register the FileAuditor MBean", e);
		}		
	}
	
	/*
	 * Ensure that the record at the given position is valid
	 */
	private boolean isCurrentRecordValid(long currentFilePosition) throws IOException
	{
		boolean validRecord = true;
		
		auditFile.seek(currentFilePosition - RECORD_METADATA_SIZE);
		
		// verify the last record is legit
		int start = auditFile.readInt();
		int size = auditFile.readInt();
		recordCount.set(auditFile.readInt()); 
		
		byte[] sha1 = new byte[20];
		auditFile.read(sha1);
		
		int end = auditFile.readInt();
		
		// verify the start and end of the meta data
		validRecord = (start == RECORD_META_WRAPPER && end == RECORD_META_WRAPPER);
		if (validRecord)
		{
			// calculate the SHA1 of the message
			auditFile.seek(currentFilePosition - RECORD_METADATA_SIZE - size);
			byte[] message = new byte[size];
			
			auditFile.read(message);
			
			// calculate the SHA1 hash of the message
			byte[] digest = generateDigest(message);
				
			validRecord = Arrays.equals(digest, sha1);
		}	
		
		// set the file position back to where we found it
		auditFile.seek(currentFilePosition);
		
		return validRecord;
	}
	
	/*
	 * generates a SHA1 digest of a message
	 */
	private byte[] generateDigest(byte[] message)
	{
		// calculate the SHA1 hash of the message
		try
		{
			MessageDigest md = MessageDigest.getInstance("SHA1");
			md.update(message);
			return md.digest();
		}
		catch (NoSuchAlgorithmException e)
		{
			return new byte[] {};
		}
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void writeEvent(UUID eventId, Calendar eventTimeStamp, String principal, AuditEvent event, Collection contexts)
	{
		///CLOVER:OFF
		if (LOGGER.isDebugEnabled())
		{
			StringBuilder builder = new StringBuilder("Attempting to write new event to the audit store.");
			builder.append("\r\n\t Event Id: ").append(eventId.toString());
			builder.append("\r\n\t Event Time: ").append(df.format(eventTimeStamp.getTime()));
			builder.append("\r\n\t Event Principal: ").append(principal);
			builder.append("\r\n\t Event Name: ").append(event.getName());
			builder.append("\r\n\t Event Type: ").append(event.getType());
			LOGGER.trace(builder.toString());
		}
		///CLOVER:ON
		
		String recordText = buildRecordText(eventId, eventTimeStamp, principal, event, contexts);
		
		byte[] messageBytes = recordText.getBytes();
		
		// generate the SHA1
		byte[] sha1 =  generateDigest(messageBytes);
		
		try
		{
			// write the message out
			auditFile.writeInt(messageBytes.length);
			auditFile.write(messageBytes);
			auditFile.writeInt(RECORD_META_WRAPPER);
			auditFile.writeInt(messageBytes.length);
			auditFile.writeInt(recordCount.incrementAndGet());
			auditFile.write(sha1);
			auditFile.writeInt(RECORD_META_WRAPPER);
		}
		catch (IOException e)
		{
			throw new IllegalStateException("The audit file cannot be written to.", e);
		}
	}

	/*
	 * builds the text of the record that will be placed in the file
	 */
	private String buildRecordText(UUID eventId, Calendar eventTimeStamp, String principal, AuditEvent event, Collection contexts)
	{
		StringBuilder builder = new StringBuilder();
		
		builder.append("\r\n" + EVENT_ID + ": " + eventId + EVENT_TAG_DELIMITER);
		builder.append("\t" + EVENT_TIME + ": " + df.format(eventTimeStamp.getTime()) + EVENT_TAG_DELIMITER);
		builder.append("\t" + EVENT_PRINCIPAL + ": " + principal + EVENT_TAG_DELIMITER);
		builder.append("\t" + EVENT_NAME + ": " + event.getName() + EVENT_TAG_DELIMITER);
		builder.append("\t" + EVENT_TYPE + ": " + event.getType() + EVENT_TAG_DELIMITER);
		
		if (contexts != null && contexts.size() > 0)
		{
			builder.append("\t" + EVENT_CTX + CONTEXT_TAG_DELIMITER);
			for (AuditContext context : contexts)
				builder.append("\t\t" + context.getContextName() + ":" + context.getContextValue() + CONTEXT_TAG_DELIMITER);
			
			builder.append(EVENT_TAG_DELIMITER);
		}
		
		builder.append("\r\n");
		
		return builder.toString();
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized Integer getEventCount() 
	{
		return recordCount.get();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized CompositeData[] getEvents(Integer eventCount) 
	{
		if (eventType == null || eventCount == 0)
			return null;
		
		Vector retVal = new Vector();
		
		/*
		 * Save off the position
		 */
		long savePosition = -1;
		try
		{
			savePosition = auditFile.getFilePointer();
			long currentPosition = savePosition;
		
			// Get the last event
			CompositeData event = getLastEvent();
			if (event != null)
				retVal.add(event);
			
			// keep getting event until either we have reached the requested count
			// or there are no more records
			int cnt = 1;			
			while (cnt < eventCount && event != null)
			{				
				int size = getRecordSize(currentPosition);
				
				// move the file pointer to the last record
				currentPosition -= (RECORD_METADATA_SIZE + size + 4);
				auditFile.seek(currentPosition);
				event = getLastEvent();
				
				if (event != null)
					retVal.add(event);				
				
				++cnt;
			}
		}
		catch (IOException e) 
		{
			/* no-op */
		}
		finally 
		{
			try
			{
				// set the file point back to the orignal position
				if (savePosition > -1)
					auditFile.seek(savePosition);
			}
			catch (IOException e) {/* no-op */}
		}		
		
		return (retVal.size() > 0) ? retVal.toArray(new CompositeData[retVal.size()]) : null;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized CompositeData getLastEvent() 
	{
		if (eventType == null)
			return null;
		
		CompositeData retVal = null;
		
		long currentPosition = -1;
		try
		{
			// save off the current position and get the last event
			currentPosition = auditFile.getFilePointer();
		
			retVal = getEvent(currentPosition);
		
		}
		catch (IOException e)
		{
			/* no-op */
		}
		finally
		{
			try
			{
				// set the file position back to the original position
				if (currentPosition > -1)
					auditFile.seek(currentPosition);
			}
			catch (IOException e) {/* no-op */}
		}
		
		return retVal;
	}
	
	/*
	 * Gets the size of a record at the before the current file posistion
	 */
	private int getRecordSize(long position)
	{
		int retVal = -1;
		
		try
		{
			long currentPosition = position;
			if (getEventCount() > 0 && currentPosition >= RECORD_METADATA_SIZE)
			{
				auditFile.seek(currentPosition - RECORD_METADATA_SIZE);
				
				auditFile.readInt();
				retVal = auditFile.readInt();
			}
		}
		catch (IOException e){}
		finally
		{
			try
			{
				// put the file pointer back in the original position
				auditFile.seek(position);
			}
			catch (IOException e) {/* no-op */}
		}
		
		return retVal;
	}
	
	/*
	 * Get the event prior to the file position
	 */
	private CompositeData getEvent(long position)
	{
		CompositeData retVal = null;
		
		try
		{
			long currentPosition = position;
			if (getEventCount() > 0 && currentPosition >= RECORD_METADATA_SIZE)
			{
				int size = this.getRecordSize(position);
				
				if (size > 0)
				{
					// go to the beginning of the record	
					auditFile.seek(currentPosition - RECORD_METADATA_SIZE - size);
		
					
					byte[] message = new byte[size];
					
					// read the message
					auditFile.read(message);					
					String strMessage = new String(message);
	
					// split into an array using the event delimiter
					String[] eventTags = strMessage.split(EVENT_TAG_DELIMITER);
					
					String id = "";
					String time = "";
					String principal = "";
					String name = "";
					String type = "";
					String[] contexts = null;
					
					for (String tag : eventTags)
					{
						tag = tag.trim();
						
						if (tag.startsWith(EVENT_ID))
							id = getItemText(tag);
						else if (tag.startsWith(EVENT_TIME))
							time = getItemText(tag);
						else if (tag.startsWith(EVENT_PRINCIPAL))
							principal = getItemText(tag);
						else if (tag.startsWith(EVENT_NAME))
							name = getItemText(tag);
						else if (tag.startsWith(EVENT_TYPE))
							type = getItemText(tag);						
						else if (tag.startsWith(EVENT_CTX))
						{
							// need to add the \r\n back on the end
							tag += "\r\n";
							String[] ctx = tag.split(CONTEXT_TAG_DELIMITER);
							if (ctx.length > 1)
							{
								contexts = new String[ctx.length - 1];
								for (int i = 1; i < ctx.length; ++i)
									contexts[i-1] = ctx[i].trim();
							}
						}
					}
					
					if (contexts == null)
						contexts = new String[] {" "};
					
					Object[] eventValues = {id, time, principal, name, type, contexts};
					
					try
					{
						// create the record to be returned
						retVal = new CompositeDataSupport(eventType, itemNames, eventValues);
					}
					catch (OpenDataException e)
					{
						LOGGER.error("Error create composit data for audit event.", e);
					}
				}			
			}
		}
		catch (IOException e)
		{
			LOGGER.error("Error reading audit file to create audit event composite data.", e);
		}
		finally 
		{
			try
			{
				// put the file pointer back in the original position
				auditFile.seek(position);
			}
			catch (IOException e) { /* no-op */} 
		}
		
		return retVal;
	}
	
	/*
	 * get the text of a specific item tag
	 */
	private String getItemText(String item)
	{
		int index = item.indexOf(":");
		if (index > -1)
		{
			return item.substring(index + 1).trim();
		}
		
		return "";
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public synchronized void clear()
	{
		// simply set the file length to 0 and put the file pointer back to the beginning of the file
		try
		{
			auditFile.setLength(0);
			auditFile.seek(0);
			recordCount.set(0);
		}
		catch (IOException e) {/*no-op */}
	}
	
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy