001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2013 Cybernetica AS 015 * Portions copyright 2014-2015 ForgeRock AS. 016 */ 017package org.forgerock.audit.handlers.syslog; 018 019import static org.forgerock.audit.util.ResourceExceptionsUtil.adapt; 020import static org.forgerock.audit.util.ResourceExceptionsUtil.notSupported; 021import static org.forgerock.json.resource.Responses.newResourceResponse; 022 023import java.net.InetSocketAddress; 024import javax.inject.Inject; 025 026import org.forgerock.audit.Audit; 027import org.forgerock.audit.events.EventTopicsMetaData; 028import org.forgerock.audit.events.handlers.AuditEventHandlerBase; 029import org.forgerock.audit.providers.DefaultLocalHostNameProvider; 030import org.forgerock.audit.providers.LocalHostNameProvider; 031import org.forgerock.audit.providers.ProductInfoProvider; 032import org.forgerock.json.JsonValue; 033import org.forgerock.json.resource.BadRequestException; 034import org.forgerock.json.resource.InternalServerErrorException; 035import org.forgerock.json.resource.NotSupportedException; 036import org.forgerock.json.resource.QueryRequest; 037import org.forgerock.json.resource.QueryResourceHandler; 038import org.forgerock.json.resource.QueryResponse; 039import org.forgerock.json.resource.ResourceException; 040import org.forgerock.json.resource.ResourceResponse; 041import org.forgerock.services.context.Context; 042import org.forgerock.util.Reject; 043import org.forgerock.util.promise.Promise; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046 047/** 048 * The handler publishes audit events formatted using {@link SyslogFormatter} to a syslog daemon using 049 * the configured {@link SyslogPublisher}. The publisher is flushed after each write. 050 */ 051public class SyslogAuditEventHandler extends AuditEventHandlerBase { 052 053 private static final Logger logger = LoggerFactory.getLogger(SyslogAuditEventHandler.class); 054 055 private final SyslogPublisher publisher; 056 private final SyslogFormatter formatter; 057 058 /** 059 * Create a new SyslogAuditEventHandler instance. 060 * 061 * @param configuration 062 * Configuration parameters that can be adjusted by system administrators. 063 * @param eventTopicsMetaData 064 * Meta-data for all audit event topics. 065 * @param productInfoProvider 066 * Provides info such as product name. 067 * @param localHostNameProvider 068 * Provides local host name. 069 */ 070 @Inject 071 public SyslogAuditEventHandler( 072 final SyslogAuditEventHandlerConfiguration configuration, 073 final EventTopicsMetaData eventTopicsMetaData, 074 @Audit final ProductInfoProvider productInfoProvider, 075 @Audit final LocalHostNameProvider localHostNameProvider) { 076 077 super(configuration.getName(), eventTopicsMetaData, configuration.getTopics(), configuration.isEnabled()); 078 Reject.ifNull(configuration.getProtocol(), 079 "Syslog transport 'protocol' of TCP or UDP is required"); 080 Reject.ifNull(configuration.getHost(), 081 "Syslog destination server 'host' is required"); 082 Reject.ifTrue(configuration.getPort() < 0 || configuration.getPort() > 65535, 083 "Syslog destination server 'port' between 0 and 65535 is required"); 084 Reject.ifNull(configuration.getFacility(), 085 "Syslog 'facility' is required"); 086 Reject.ifTrue(configuration.getProtocol() == TransportProtocol.TCP && configuration.getConnectTimeout() == 0, 087 "Syslog 'connectTimeout' is required for TCP connections"); 088 089 InetSocketAddress socketAddress = new InetSocketAddress(configuration.getHost(), configuration.getPort()); 090 this.publisher = configuration.getProtocol().getPublisher(socketAddress, configuration); 091 this.formatter = new SyslogFormatter( 092 eventTopicsMetaData, 093 configuration, 094 getLocalHostNameProvider(localHostNameProvider), 095 getProductNameProvider(productInfoProvider)); 096 097 logger.debug("Successfully configured Syslog audit event handler."); 098 } 099 100 private ProductInfoProvider getProductNameProvider(ProductInfoProvider productInfoProvider) { 101 if (productInfoProvider != null) { 102 return productInfoProvider; 103 } else { 104 logger.debug("No {} provided; using default.", ProductInfoProvider.class.getSimpleName()); 105 return new DefaultProductInfoProvider(); 106 } 107 } 108 109 private LocalHostNameProvider getLocalHostNameProvider(LocalHostNameProvider localHostNameProvider) { 110 if (localHostNameProvider != null) { 111 return localHostNameProvider; 112 } else { 113 logger.debug("No {} provided; using default.", LocalHostNameProvider.class.getSimpleName()); 114 return new DefaultLocalHostNameProvider(); 115 } 116 } 117 118 /** {@inheritDoc} */ 119 @Override 120 public void startup() { 121 // nothing to do 122 } 123 124 /** 125 * Closes the connections established by {@link SyslogPublisher}. 126 */ 127 @Override 128 public void shutdown() { 129 synchronized (publisher) { 130 publisher.close(); 131 } 132 } 133 134 @Override 135 public Promise<ResourceResponse, ResourceException> publishEvent(Context context, String topic, JsonValue event) { 136 137 try { 138 final String syslogMessage = formatAsSyslogMessage(topic, event); 139 synchronized (publisher) { 140 publisher.publishMessage(syslogMessage); 141 } 142 143 return newResourceResponse( 144 event.get(ResourceResponse.FIELD_CONTENT_ID).asString(), 145 null, 146 event.clone()).asPromise(); 147 148 } catch (Exception ex) { 149 return adapt(ex).asPromise(); 150 } 151 } 152 153 private String formatAsSyslogMessage(String topic, JsonValue auditEvent) throws ResourceException { 154 if (!formatter.canFormat(topic)) { 155 throw new InternalServerErrorException("Unable to format " + topic + " audit event"); 156 } 157 try { 158 return formatter.format(topic, auditEvent); 159 } catch (Exception ex) { 160 throw new BadRequestException(ex); 161 } 162 } 163 164 @Override 165 public Promise<QueryResponse, ResourceException> queryEvents( 166 Context context, 167 String topic, 168 QueryRequest queryRequest, 169 QueryResourceHandler queryResourceHandler) { 170 return notSupported(queryRequest).asPromise(); 171 } 172 173 @Override 174 public Promise<ResourceResponse, ResourceException> readEvent(Context context, String topic, String resourceId) { 175 return new NotSupportedException("query operations are not supported").asPromise(); 176 } 177 178 /** 179 * Default implementation of ProductNameProvider. 180 */ 181 private static class DefaultProductInfoProvider implements ProductInfoProvider { 182 183 @Override 184 public String getProductName() { 185 return null; 186 } 187 } 188}