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-2016 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}