/* * * * JBoss, Home of Professional Open Source. * * Copyright 2013, Red Hat, Inc., and individual contributors * * as indicated by the @author tags. See the copyright.txt file in the * * distribution for a full listing of individual contributors. * * * * This is free software; you can redistribute it and/or modify it * * under the terms of the GNU Lesser General Public License as * * published by the Free Software Foundation; either version 2.1 of * * the License, or (at your option) any later version. * * * * This software 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 * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public * * License along with this software; if not, write to the Free * * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * */ package org.jboss.as.controller.audit; import java.io.IOException; import java.net.InetAddress; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.jboss.as.controller.OperationContext.ResultAction; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.audit.SyslogAuditLogHandler.Facility; import org.jboss.as.controller.logging.ControllerLogger; import org.jboss.as.controller.registry.Resource; import org.jboss.as.core.security.AccessMechanism; import org.jboss.dmr.ModelNode; /** * Audit logger wrapper * * @author Brian Stansberry (c) 2012 Red Hat Inc. * @author Kabir Khan */ public class ManagedAuditLoggerImpl implements ManagedAuditLogger, ManagedAuditLogger.AuditLogHandlerUpdater { /** Maximum number of consecutive logging failures before we stop logging */ private static final short MAX_FAILURE_COUNT = 10; private final List<ManagedAuditLoggerImpl> childImpls; /** If we are the core audit logger, list the children */ private final ManagedAuditLogConfiguration config; /** Guarded by config's auditLock - updates to the handlers */ private HandlerUpdateTask handlerUpdateTask; /** Guarded by config's auditLock - the messages logged while in the QUEUEING state */ private final List<AuditLogItem> queuedItems = new ArrayList<AuditLogItem>(); /** Guarded by config's auditLock - the number of failures writing to the log */ private short failureCount; /** Whether we can ignore logging without taking the lock to check. * Reliant on loggerStatus not being changed elsewhere, since it is not shared * Only change with lock held * Must be reset to false when handler updates need to be performed */ private final AtomicBoolean runDisabledFastPath = new AtomicBoolean(false); public ManagedAuditLoggerImpl(String asVersion, boolean server) { config = new CoreAuditLogConfiguration(asVersion, server); childImpls = new ArrayList<ManagedAuditLoggerImpl>(); } private ManagedAuditLoggerImpl(ManagedAuditLoggerImpl src, boolean manualCommit) { assert src.config instanceof CoreAuditLogConfiguration : "Not an instance of CoreAuditLogConfiguration"; config = new NewAuditLogConfiguration((CoreAuditLogConfiguration)src.config, manualCommit); childImpls = null; } @Override public void log(boolean readOnly, ResultAction resultAction, String userId, String domainUUID, AccessMechanism accessMechanism, InetAddress remoteAddress, Resource resultantModel, List<ModelNode> operations) { if (runDisabledFastPath.get()) return; config.lock(); try { if (skipLogging(readOnly)) { return; } storeLogItem( AuditLogItem.createModelControllerItem(config.getAsVersion(), readOnly, config.isBooting(), resultAction, userId, domainUUID, accessMechanism, remoteAddress, resultantModel, operations)); } catch (Exception e) { handleLoggingException(e); } finally { applyHandlerUpdates(); config.unlock(); } } @Override public void logJmxMethodAccess(boolean readOnly, String userId, String domainUUID, AccessMechanism accessMechanism, InetAddress remoteAddress, String methodName, String[] methodSignature, Object[] methodParams, Throwable error) { if (runDisabledFastPath.get()) return; config.lock(); try { if (skipLogging(readOnly)) { return; } storeLogItem( AuditLogItem.createMethodAccessItem(config.getAsVersion(), readOnly, config.isBooting(), userId, domainUUID, accessMechanism, remoteAddress, methodName, methodSignature, methodParams, error)); } catch (Exception e) { handleLoggingException(e); } finally { applyHandlerUpdates(); config.unlock(); } } private boolean skipLogging(boolean readOnly) { if (config.isBooting() && !isLogBoot() || readOnly && !isLogReadOnly()) { if (getLoggerStatus() == Status.DISABLED) { // switch to the fast path for the next event runDisabledFastPath.set(true); } return true; } return false; } public ManagedAuditLoggerImpl createNewConfiguration(boolean manualCommit) { if (childImpls == null) { throw ControllerLogger.ROOT_LOGGER.canOnlyCreateChildAuditLoggerForMainAuditLogger(); } ManagedAuditLoggerImpl child = new ManagedAuditLoggerImpl(this, manualCommit); childImpls.add(child); return child; } public boolean isLogReadOnly() { config.lock(); try { return config.isLogReadOnly(); } finally { config.unlock(); } } public void setLogReadOnly(boolean logReadOnly) { config.lock(); try { config.setLogReadOnly(logReadOnly); } finally { config.unlock(); } } public boolean isLogBoot() { config.lock(); try { return config.isLogBoot(); } finally { config.unlock(); } } public void setLogBoot(boolean logBoot) { config.lock(); try { config.setLogBoot(logBoot); if (logBoot == false && config.isBooting() && !config.isLogBoot()) { queuedItems.clear(); } } finally { config.unlock(); } } public Status getLoggerStatus() { config.lock(); try { return config.getLoggerStatus(); } finally { config.unlock(); } } @Override public void recycleHandler(String name) { config.lock(); try { config.recycleHandler(name); } finally { config.unlock(); } } @Override public void setLoggerStatus(final Status newStatus) { config.lock(); try { if (newStatus == Status.DISABLE_NEXT && config.getLoggerStatus() == Status.DISABLED) { return; } config.setLoggerStatus(newStatus); if (newStatus == Status.LOGGING){ for (AuditLogItem record : queuedItems) { try { writeLogItem(record); } catch (Exception e) { handleLoggingException(e); } } } else if (newStatus == Status.DISABLED){ queuedItems.clear(); } runDisabledFastPath.set(false); } finally { config.unlock(); } } /** protected by config's audit lock */ private void storeLogItem(AuditLogItem item) throws IOException { switch (getLoggerStatus()) { case QUEUEING: queuedItems.add(item); break; case LOGGING: writeLogItem(item); break; case DISABLE_NEXT: writeLogItem(item); config.setLoggerStatus(Status.DISABLED); case DISABLED: // switch to the fast path for the next event runDisabledFastPath.set(true); break; } } /** protected by config's audit lock */ private void writeLogItem(AuditLogItem item) throws IOException{ Set<String> formatterNames = new HashSet<String>(); try { for (AuditLogHandler handler : config.getHandlersForLogging()) { formatterNames.add(handler.getFormatterName()); handler.writeLogItem(item); } } finally { for (String formatterName : formatterNames) { config.getFormatter(formatterName).clear(); } } } /** protected by config's audit lock */ private void handleLoggingException(final Exception e) { ControllerLogger.MGMT_OP_LOGGER.failedToUpdateAuditLog(e); if (++failureCount == MAX_FAILURE_COUNT) { // Continuous failure likely indicates some configuration problem setLoggerStatus(Status.DISABLED); ControllerLogger.MGMT_OP_LOGGER.disablingLoggingDueToFailures(failureCount); } } public AuditLogHandlerUpdater getUpdater() { return this; } @Override public void addHandler(AuditLogHandler handler) { config.lock(); try { if (handlerUpdateTask == null){ handlerUpdateTask = new HandlerUpdateTask(); } handlerUpdateTask.addHandler(handler); runDisabledFastPath.set(false); } finally { config.unlock(); } } @Override public void updateHandler(AuditLogHandler handler) { config.lock(); try { AuditLogHandler existing = config.getConfiguredHandler(handler.getName()); if (handler.isDifferent(existing)){ if (handlerUpdateTask == null){ handlerUpdateTask = new HandlerUpdateTask(); } handlerUpdateTask.replaceHandler(handler); runDisabledFastPath.set(false); } } finally { config.unlock(); } } @Override public void removeHandler(String name) { config.lock(); try { if (handlerUpdateTask == null){ handlerUpdateTask = new HandlerUpdateTask(); } handlerUpdateTask.removeHandler(name); runDisabledFastPath.set(false); } finally { config.unlock(); } } @Override public void addHandlerReference(PathAddress referenceAddress) { config.lock(); try { if (handlerUpdateTask == null){ handlerUpdateTask = new HandlerUpdateTask(); } handlerUpdateTask.addHandlerReference(referenceAddress); runDisabledFastPath.set(false); } finally { config.unlock(); } } @Override public void removeHandlerReference(PathAddress referenceAddress) { config.lock(); try { if (handlerUpdateTask == null){ handlerUpdateTask = new HandlerUpdateTask(); } handlerUpdateTask.removeHandlerReference(referenceAddress); runDisabledFastPath.set(false); } finally { config.unlock(); } } @Override public void rollbackChanges() { config.lock(); try { if (handlerUpdateTask != null){ handlerUpdateTask.rollbackChanges(); handlerUpdateTask = null; } } finally { config.unlock(); } } @Override public void applyChanges() { config.lock(); try { if (!config.isManualCommit()) { //i18n not needed, normal users will never end up here throw new IllegalStateException("Attempt was made to manually apply changes when manual commit was not configured"); } applyHandlerUpdates(); } finally { config.unlock(); } } /** Call with lock taken */ private void applyHandlerUpdates() { if (handlerUpdateTask != null) { handlerUpdateTask.applyChanges(); handlerUpdateTask = null; } } @Override public void removeFormatter(String name) { config.lock(); try { config.removeFormatter(name); } finally { config.unlock(); } } @Override public void addFormatter(AuditLogItemFormatter formatter) { config.lock(); try { config.addFormatter(formatter); } finally { config.unlock(); } } @Override public void updateHandlerFormatter(String name, String formatterName) { config.lock(); try { AuditLogHandler handler = config.getConfiguredHandler(name); handler.setFormatterName(formatterName); handler.setFormatter(config.getFormatter(formatterName)); } finally { config.unlock(); } } // Immediate updates - TODO find some better way to do these if we end up adding more handler types! @Override public void updateHandlerMaxFailureCount(String name, int count) { config.lock(); try { AuditLogHandler handler = config.getConfiguredHandler(name); handler.setMaxFailureCount(count); } finally { config.unlock(); } } @Override public int getHandlerFailureCount(String name) { config.lock(); try { AuditLogHandler handler = config.getConfiguredHandler(name); return handler.getFailureCount(); } finally { config.unlock(); } } @Override public void updateSyslogHandlerFacility(String name, Facility facility) { config.lock(); try { SyslogAuditLogHandler handler = (SyslogAuditLogHandler)config.getConfiguredHandler(name); handler.setFacility(facility); } finally { config.unlock(); } } @Override public void updateSyslogHandlerAppName(String name, String appName) { config.lock(); try { SyslogAuditLogHandler handler = (SyslogAuditLogHandler)config.getConfiguredHandler(name); handler.setAppName(appName); } finally { config.unlock(); } } @Override public void updateSyslogHandlerReconnectTimeout(String name, int reconnectTimeout) { config.lock(); try { SyslogAuditLogHandler handler = (SyslogAuditLogHandler)config.getConfiguredHandler(name); handler.setReconnectTimeout(reconnectTimeout); } finally { config.unlock(); } } // Immediate updates @Override public boolean getHandlerDisabledDueToFailure(String name) { config.lock(); try { AuditLogHandler handler = config.getConfiguredHandler(name); return handler.isDisabledDueToFailures(); } finally { config.unlock(); } } @Override public JsonAuditLogItemFormatter getJsonFormatter(String name) { config.lock(); try { return (JsonAuditLogItemFormatter)config.getFormatter(name); } finally { config.unlock(); } } @Override public List<ModelNode> listLastEntries(String name) { config.lock(); try { return config.getConfiguredHandler(name).listLastEntries(); } finally { config.unlock(); } } @Override public void updateInMemoryHandlerMaxHistory(String name, int maxHistory) { config.lock(); try { InMemoryAuditLogHander handler = (InMemoryAuditLogHander)config.getConfiguredHandler(name); handler.setMaxHistory(maxHistory); } finally { config.unlock(); } } /** * Abstract base class for core and new configuration * */ abstract static class ManagedAuditLogConfiguration { final boolean manualCommit; final boolean core; final SharedConfiguration sharedConfiguration; private volatile Status status = Status.QUEUEING; private volatile boolean logBoot = true; private volatile boolean logReadOnly; /** Guarded by auditLock - the configured handlers we should use */ protected final Set<String> handlerReferences = Collections.synchronizedSet(new HashSet<String>()); ManagedAuditLogConfiguration(SharedConfiguration sharedConfiguration, boolean core, boolean manualCommit) { this.sharedConfiguration = sharedConfiguration; this.core = core; this.manualCommit = manualCommit; } boolean isManualCommit() { return manualCommit; } boolean hasHandlerReference(String name) { return handlerReferences.contains(name); } void addHandlerReference(String name) { handlerReferences.add(name); } void removeHandlerReference(String name) { handlerReferences.remove(name); } List<AuditLogHandler> getHandlersForLogging(){ List<AuditLogHandler> list = new ArrayList<>(); for (Map.Entry<String, AuditLogHandler> handlerEntry : sharedConfiguration.getConfiguredHandlers().entrySet()) { if (handlerEntry.getValue().isActive() && hasHandlerReference(handlerEntry.getKey())) { list.add(handlerEntry.getValue()); } } return list; } void lock() { sharedConfiguration.lock(); } void unlock() { sharedConfiguration.unlock(); } String getAsVersion() { return sharedConfiguration.getAsVersion(); } boolean isServer() { return sharedConfiguration.isServer(); } /** Call with lock taken */ AuditLogItemFormatter getFormatter(String name) { return sharedConfiguration.getFormatter(name); } /** Call with lock taken */ AuditLogHandler getConfiguredHandler(String name) { return sharedConfiguration.getConfiguredHandler(name); } /** Call with lock taken */ void setBooting(boolean booting) { sharedConfiguration.setBooting(booting); } /** Call with lock taken */ boolean isBooting() { return sharedConfiguration.isBooting(); } /** Call with lock taken */ boolean isLogReadOnly() { return logReadOnly; } /** Call with lock taken */ void setLogReadOnly(boolean logReadOnly) { this.logReadOnly = logReadOnly; } /** Call with lock taken */ boolean isLogBoot() { return logBoot; } /** Call with lock taken */ void setLogBoot(boolean logBoot) { this.logBoot = logBoot; } /** Call with lock taken */ Status getLoggerStatus() { return status; } /** Call with lock taken */ void setLoggerStatus(final Status newStatus) { this.status = newStatus; } boolean isCore() { return core; } abstract void putConfiguredHandler(AuditLogHandler handler); abstract AuditLogHandler removeConfiguredHandler(String name); abstract void addFormatter(AuditLogItemFormatter formatter); abstract void removeFormatter(String name); abstract void recycleHandler(String name); } /** * The configuration for the core audit logger */ private static class CoreAuditLogConfiguration extends ManagedAuditLogConfiguration { CoreAuditLogConfiguration(String asVersion, boolean server) { super(new SharedConfiguration(asVersion, server), true, false); } @Override void putConfiguredHandler(AuditLogHandler handler) { sharedConfiguration.putConfiguredHandler(handler); } @Override AuditLogHandler removeConfiguredHandler(String name) { return sharedConfiguration.removeConfiguredHandler(name); } void addFormatter(AuditLogItemFormatter formatter) { sharedConfiguration.addFormatter(formatter); } void removeFormatter(String name) { sharedConfiguration.removeFormatter(name); } void recycleHandler(String name) { sharedConfiguration.recycleHandler(name); } } /** * The configuration for other audit loggers, e.g. jmx */ private static class NewAuditLogConfiguration extends ManagedAuditLogConfiguration { NewAuditLogConfiguration(CoreAuditLogConfiguration core, boolean manualCommit) { super(core.sharedConfiguration, false, manualCommit); } @Override void putConfiguredHandler(AuditLogHandler handler) { //i18n not needed, this will not be called by user code throw new IllegalStateException("Only available in core configuration"); } @Override AuditLogHandler removeConfiguredHandler(String name) { //i18n not needed, this will not be called by user code throw new IllegalStateException("Only available in core configuration"); } @Override void addFormatter(AuditLogItemFormatter formatter) { //i18n not needed, this will not be called by user code throw new IllegalStateException("Only available in core configuration"); } @Override void removeFormatter(String name) { //i18n not needed, this will not be called by user code throw new IllegalStateException("Only available in core configuration"); } @Override void recycleHandler(String name) { //i18n not needed, this will not be called by user code throw new IllegalStateException("Only available in core configuration"); } } /** * Configuration shared among all the configurations and all access to methods will take place with the lock taken. */ private static class SharedConfiguration { /** Should be fair to maintain order. Shared among all configurations */ private final Lock auditLock = new ReentrantLock(true); private final String asVersion; private final boolean server; /** Guarded by auditLock - the formatters configured in the global json-formatters section */ private final Map<String, AuditLogItemFormatter> formatters = new HashMap<String, AuditLogItemFormatter>(); /** Guarded by auditLock - the handlers configured in the global file-handlers and syslog-handlers section */ private final Map<String, AuditLogHandler> configuredHandlers = new HashMap<String, AuditLogHandler>(); /** Guarded by auditLock - whether we are boothing or not */ private boolean booting = true; SharedConfiguration(String asVersion, boolean server) { this.asVersion = asVersion; this.server = server; } public void recycleHandler(String name) { AuditLogHandler handler = configuredHandlers.get(name); handler.recycle(); } void lock() { auditLock.lock(); } void unlock() { auditLock.unlock(); } String getAsVersion() { return asVersion; } boolean isServer() { return server; } Map<String, AuditLogHandler> getConfiguredHandlers() { return configuredHandlers; } AuditLogItemFormatter getFormatter(String name) { return formatters.get(name); } void addFormatter(AuditLogItemFormatter formatter) { formatters.put(formatter.getName(), formatter); } void removeFormatter(String name) { formatters.remove(name); } AuditLogHandler getConfiguredHandler(String name) { return configuredHandlers.get(name); } void putConfiguredHandler(AuditLogHandler handler) { configuredHandlers.put(handler.getName(), handler); } AuditLogHandler removeConfiguredHandler(String name) { return configuredHandlers.remove(name); } void setBooting(boolean booting) { this.booting = booting; } boolean isBooting() { return booting; } } /** * When we add a handler(reference) we want that to be part of the current write. * If we remove/change and handler, and or reference, we don't want that to take effect until the next write. */ private class HandlerUpdateTask { private Map<String, AuditLogHandler> addedHandlers; private Map<String, AuditLogHandler> replacedHandlers; private Set<String> removedHandlers; private Set<PathAddress> addedReferences; private Set<PathAddress> removedReferences; private Map<String, String> updatedFormatters; void addHandler(AuditLogHandler handler){ assert config.isCore() : "Not available for non-core configuration"; if (removedHandlers != null && removedHandlers.contains(handler.getName())) { throw ControllerLogger.ROOT_LOGGER.attemptToBothRemoveAndAddHandlerUpdateInstead(); } if (addedHandlers == null){ addedHandlers = new HashMap<String, AuditLogHandler>(); } addedHandlers.put(handler.getName(), handler); //Update the 'live' handlers with the addition config.putConfiguredHandler(handler); handler.setFormatter(config.getFormatter(handler.getFormatterName())); } void replaceHandler(AuditLogHandler handler){ assert config.isCore() : "Not available for non-core configuration"; if (addedHandlers != null && addedHandlers.containsKey(handler.getName())){ //We may have added say a syslog-handler, and end up here as part of adding its protocol in a separate step addHandler(handler); return; } if (replacedHandlers == null){ replacedHandlers = new HashMap<String, AuditLogHandler>(); } replacedHandlers.put(handler.getName(), handler); } void removeHandler(String name) { assert config.isCore() : "Not available for non-core configuration"; if (addedHandlers != null && addedHandlers.containsKey(name)){ throw ControllerLogger.ROOT_LOGGER.attemptToBothAddAndRemoveAndHandlerFromCompositeOperation(); } if (replacedHandlers != null && replacedHandlers.containsKey(name)){ throw ControllerLogger.ROOT_LOGGER.attemptToBothUpdateAndRemoveHandlerFromCompositeOperation(); } final AuditLogHandler handler = config.getConfiguredHandler(name); if (handler != null){ Set<PathAddress> references = handler.getReferences(); if (references.size() > 0){ if (!references.containsAll(removedReferences)){ Set<PathAddress> activeReferences = new HashSet<PathAddress>(references); activeReferences.removeAll(removedReferences); throw ControllerLogger.ROOT_LOGGER.handlerIsReferencedBy(removedReferences); } } } config.removeConfiguredHandler(name); if (removedHandlers == null){ removedHandlers = new HashSet<String>(); } removedHandlers.add(name); } void addHandlerReference(PathAddress referenceAddress){ if (removedReferences != null && removedReferences.contains(referenceAddress)){ throw ControllerLogger.ROOT_LOGGER.attemptToBothRemoveAndAddHandlerReferenceFromCompositeOperation(); } if (addedReferences == null){ addedReferences = new HashSet<PathAddress>(); } addedReferences.add(referenceAddress); //Update the 'live' handlers and references with the addition config.addHandlerReference(org.jboss.as.controller.operations.common.Util.getNameFromAddress(referenceAddress)); final String name = org.jboss.as.controller.operations.common.Util.getNameFromAddress(referenceAddress); final AuditLogHandler handler = config.getConfiguredHandler(name); if (handler == null){ throw ControllerLogger.ROOT_LOGGER.noHandlerCalled(name); } handler.addReference(referenceAddress); } void removeHandlerReference(PathAddress address){ if (addedReferences != null && addedReferences.contains(address)){ throw ControllerLogger.ROOT_LOGGER.attemptToBothRemoveAndAddHandlerReferenceFromCompositeOperation(); } if (removedReferences == null){ removedReferences = new HashSet<PathAddress>(); } removedReferences.add(address); } void rollbackChanges(){ if (addedReferences != null && addedReferences.size() > 0){ for (PathAddress address : addedReferences) { final String name = org.jboss.as.controller.operations.common.Util.getNameFromAddress(address); config.removeHandlerReference(name); AuditLogHandler handler = config.getConfiguredHandler(name); if (handler != null){ handler.removeReference(address); } } } if (addedHandlers != null && addedHandlers.size() > 0){ for (AuditLogHandler handler : addedHandlers.values()){ config.removeConfiguredHandler(handler.getName()); } } } void applyChanges() { if (removedHandlers != null && removedHandlers.size() > 0){ for (String name : removedHandlers) { AuditLogHandler handler = config.removeConfiguredHandler(name); if (handler != null){ handler.stop(); } } } if (replacedHandlers != null && replacedHandlers.size() > 0){ for (AuditLogHandler handler : replacedHandlers.values()) { AuditLogHandler existing = config.removeConfiguredHandler(handler.getName()); if (existing != null){ existing.stop(); } //Update the references for the replaced one for (PathAddress referenceAddress : existing.getReferences()){ if (removedReferences != null && !removedReferences.contains(referenceAddress)){ handler.addReference(referenceAddress); } } config.putConfiguredHandler(handler); handler.setFormatter(config.getFormatter(handler.getFormatterName())); } } if (removedReferences != null && removedReferences.size() > 0){ for (PathAddress referenceAddress : removedReferences){ final String name = org.jboss.as.controller.operations.common.Util.getNameFromAddress(referenceAddress); final AuditLogHandler handler = config.getConfiguredHandler(name); if (handler != null){ handler.removeReference(referenceAddress); } config.removeHandlerReference(name); } } } } @Override public void bootDone() { config.lock(); try { config.setBooting(false); if (childImpls != null) { for (ManagedAuditLogger child : childImpls) { child.bootDone(); } } if (config.getLoggerStatus() == Status.QUEUEING) { //There was no configuration to either enable or disable the logger during boot, so disable it setLoggerStatus(AuditLogger.Status.DISABLED); } } finally { config.unlock(); } } @Override public void startBoot() { config.lock(); try { config.setBooting(true); if (childImpls != null) { childImpls.clear(); } config.setLoggerStatus(Status.QUEUEING); } finally { config.unlock(); } } }