View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2013 Cybernetica AS
15   * Portions copyright 2014-2016 ForgeRock AS.
16   */
17  package org.forgerock.audit.handlers.syslog;
18  
19  import static org.forgerock.audit.util.ResourceExceptionsUtil.adapt;
20  import static org.forgerock.audit.util.ResourceExceptionsUtil.notSupported;
21  import static org.forgerock.json.resource.Responses.newResourceResponse;
22  
23  import jakarta.inject.Inject;
24  import java.net.InetSocketAddress;
25  import org.forgerock.audit.Audit;
26  import org.forgerock.audit.events.EventTopicsMetaData;
27  import org.forgerock.audit.events.handlers.AuditEventHandlerBase;
28  import org.forgerock.audit.providers.DefaultLocalHostNameProvider;
29  import org.forgerock.audit.providers.LocalHostNameProvider;
30  import org.forgerock.audit.providers.ProductInfoProvider;
31  import org.forgerock.json.JsonValue;
32  import org.forgerock.json.resource.BadRequestException;
33  import org.forgerock.json.resource.InternalServerErrorException;
34  import org.forgerock.json.resource.NotSupportedException;
35  import org.forgerock.json.resource.QueryRequest;
36  import org.forgerock.json.resource.QueryResourceHandler;
37  import org.forgerock.json.resource.QueryResponse;
38  import org.forgerock.json.resource.ResourceException;
39  import org.forgerock.json.resource.ResourceResponse;
40  import org.forgerock.services.context.Context;
41  import org.forgerock.util.Reject;
42  import org.forgerock.util.promise.Promise;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  /**
47   * The handler publishes audit events formatted using {@link SyslogFormatter} to a syslog daemon using
48   * the configured {@link SyslogPublisher}. The publisher is flushed after each write.
49   */
50  public class SyslogAuditEventHandler extends AuditEventHandlerBase {
51  
52      private static final Logger logger = LoggerFactory.getLogger(SyslogAuditEventHandler.class);
53  
54      private final SyslogPublisher publisher;
55      private final SyslogFormatter formatter;
56  
57      /**
58       * Create a new SyslogAuditEventHandler instance.
59       *
60       * @param configuration
61       *          Configuration parameters that can be adjusted by system administrators.
62       * @param eventTopicsMetaData
63       *          Meta-data for all audit event topics.
64       * @param productInfoProvider
65       *          Provides info such as product name.
66       * @param localHostNameProvider
67       *          Provides local host name.
68       */
69      @Inject
70      public SyslogAuditEventHandler(
71              final SyslogAuditEventHandlerConfiguration configuration,
72              final EventTopicsMetaData eventTopicsMetaData,
73              @Audit final ProductInfoProvider productInfoProvider,
74              @Audit final LocalHostNameProvider localHostNameProvider) {
75  
76          super(configuration.getName(), eventTopicsMetaData, configuration.getTopics(), configuration.isEnabled());
77          Reject.ifNull(configuration.getProtocol(),
78                  "Syslog transport 'protocol' of TCP or UDP is required");
79          Reject.ifNull(configuration.getHost(),
80                  "Syslog destination server 'host' is required");
81          Reject.ifTrue(configuration.getPort() < 0 || configuration.getPort() > 65535,
82                  "Syslog destination server 'port' between 0 and 65535 is required");
83          Reject.ifNull(configuration.getFacility(),
84                  "Syslog 'facility' is required");
85          Reject.ifTrue(configuration.getProtocol() == TransportProtocol.TCP && configuration.getConnectTimeout() == 0,
86                  "Syslog 'connectTimeout' is required for TCP connections");
87  
88          InetSocketAddress socketAddress = new InetSocketAddress(configuration.getHost(), configuration.getPort());
89          this.publisher = configuration.getProtocol().getPublisher(socketAddress, configuration);
90          this.formatter = new SyslogFormatter(
91                  eventTopicsMetaData,
92                  configuration,
93                  getLocalHostNameProvider(localHostNameProvider),
94                  getProductNameProvider(productInfoProvider));
95  
96          logger.debug("Successfully configured Syslog audit event handler.");
97      }
98  
99      private ProductInfoProvider getProductNameProvider(ProductInfoProvider productInfoProvider) {
100         if (productInfoProvider != null) {
101             return productInfoProvider;
102         } else {
103             logger.debug("No {} provided; using default.", ProductInfoProvider.class.getSimpleName());
104             return new DefaultProductInfoProvider();
105         }
106     }
107 
108     private LocalHostNameProvider getLocalHostNameProvider(LocalHostNameProvider localHostNameProvider) {
109         if (localHostNameProvider != null) {
110             return localHostNameProvider;
111         } else {
112             logger.debug("No {} provided; using default.", LocalHostNameProvider.class.getSimpleName());
113             return new DefaultLocalHostNameProvider();
114         }
115     }
116 
117     /** {@inheritDoc} */
118     @Override
119     public void startup() {
120         // nothing to do
121     }
122 
123     /**
124      * Closes the connections established by {@link SyslogPublisher}.
125      */
126     @Override
127     public void shutdown() {
128         synchronized (publisher) {
129             publisher.close();
130         }
131     }
132 
133     @Override
134     public Promise<ResourceResponse, ResourceException> publishEvent(Context context, String topic, JsonValue event) {
135 
136         try {
137             final String syslogMessage = formatAsSyslogMessage(topic, event);
138             synchronized (publisher) {
139                 publisher.publishMessage(syslogMessage);
140             }
141 
142             return newResourceResponse(
143                     event.get(ResourceResponse.FIELD_CONTENT_ID).asString(),
144                     null,
145                     event.clone()).asPromise();
146 
147         } catch (Exception ex) {
148             return adapt(ex).asPromise();
149         }
150     }
151 
152     private String formatAsSyslogMessage(String topic, JsonValue auditEvent) throws ResourceException {
153         if (!formatter.canFormat(topic)) {
154             throw new InternalServerErrorException("Unable to format " + topic + " audit event");
155         }
156         try {
157             return formatter.format(topic, auditEvent);
158         } catch (Exception ex) {
159             throw new BadRequestException(ex);
160         }
161     }
162 
163     @Override
164     public Promise<QueryResponse, ResourceException> queryEvents(
165             Context context,
166             String topic,
167             QueryRequest queryRequest,
168             QueryResourceHandler queryResourceHandler) {
169         return notSupported(queryRequest).asPromise();
170     }
171 
172     @Override
173     public Promise<ResourceResponse, ResourceException> readEvent(Context context, String topic, String resourceId) {
174         return new NotSupportedException("query operations are not supported").asPromise();
175     }
176 
177     /**
178      * Default implementation of ProductNameProvider.
179      */
180     private static class DefaultProductInfoProvider implements ProductInfoProvider {
181 
182         @Override
183         public String getProductName() {
184             return null;
185         }
186     }
187 }