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 2015-2016 ForgeRock AS.
015 */
016package org.opends.server.loggers;
017
018import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
019
020import static java.util.Arrays.asList;
021import static org.opends.messages.LoggerMessages.*;
022import static org.forgerock.audit.AuditServiceBuilder.newAuditService;
023import static org.forgerock.audit.events.EventTopicsMetaDataBuilder.coreTopicSchemas;
024import static org.forgerock.audit.json.AuditJsonConfig.registerHandlerToService;
025import static org.opends.server.util.StaticUtils.getFileForPath;
026
027import java.io.BufferedInputStream;
028import java.io.BufferedReader;
029import java.io.File;
030import java.io.FileInputStream;
031import java.io.FileReader;
032import java.io.IOException;
033import java.io.InputStream;
034import java.util.ArrayList;
035import java.util.Collections;
036import java.util.HashMap;
037import java.util.List;
038import java.util.Map;
039import java.util.SortedSet;
040import java.util.concurrent.ConcurrentHashMap;
041import java.util.concurrent.atomic.AtomicBoolean;
042import java.util.regex.Pattern;
043
044import org.forgerock.audit.AuditException;
045import org.forgerock.audit.AuditService;
046import org.forgerock.audit.AuditServiceBuilder;
047import org.forgerock.audit.AuditServiceConfiguration;
048import org.forgerock.audit.AuditServiceProxy;
049import org.forgerock.audit.DependencyProvider;
050import org.forgerock.audit.events.EventTopicsMetaData;
051import org.forgerock.audit.events.handlers.FileBasedEventHandlerConfiguration.FileRetention;
052import org.forgerock.audit.events.handlers.FileBasedEventHandlerConfiguration.FileRotation;
053import org.forgerock.audit.filter.FilterPolicy;
054import org.forgerock.audit.handlers.csv.CsvAuditEventHandler;
055import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration;
056import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration.CsvFormatting;
057import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration.CsvSecurity;
058import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration.EventBufferingConfiguration;
059import org.forgerock.audit.json.AuditJsonConfig;
060import org.forgerock.i18n.slf4j.LocalizedLogger;
061import org.forgerock.json.JsonValue;
062import org.forgerock.json.resource.RequestHandler;
063import org.forgerock.opendj.config.ConfigurationFramework;
064import org.forgerock.opendj.config.server.ConfigException;
065import org.opends.server.admin.server.ServerManagementContext;
066import org.opends.server.admin.std.server.CsvFileAccessLogPublisherCfg;
067import org.opends.server.admin.std.server.CsvFileHTTPAccessLogPublisherCfg;
068import org.opends.server.admin.std.server.ExternalAccessLogPublisherCfg;
069import org.opends.server.admin.std.server.ExternalHTTPAccessLogPublisherCfg;
070import org.opends.server.admin.std.server.FileCountLogRetentionPolicyCfg;
071import org.opends.server.admin.std.server.FixedTimeLogRotationPolicyCfg;
072import org.opends.server.admin.std.server.FreeDiskSpaceLogRetentionPolicyCfg;
073import org.opends.server.admin.std.server.LogPublisherCfg;
074import org.opends.server.admin.std.server.LogRetentionPolicyCfg;
075import org.opends.server.admin.std.server.LogRotationPolicyCfg;
076import org.opends.server.admin.std.server.RootCfg;
077import org.opends.server.admin.std.server.SizeLimitLogRetentionPolicyCfg;
078import org.opends.server.admin.std.server.SizeLimitLogRotationPolicyCfg;
079import org.opends.server.admin.std.server.TimeLimitLogRotationPolicyCfg;
080import org.opends.server.config.ConfigEntry;
081import org.opends.server.core.DirectoryServer;
082import org.forgerock.opendj.ldap.DN;
083import org.opends.server.util.StaticUtils;
084
085/**
086 * Entry point for the common audit facility.
087 * <p>
088 * This class manages the AuditService instances and Audit Event Handlers that correspond to the
089 * publishers defined in OpenDJ configuration.
090 * <p>
091 * In theory there should be only one instance of AuditService for all the event handlers but
092 * defining one service per handler allow to perform filtering at the DJ server level.
093 */
094public class CommonAudit
095{
096  /** Transaction id used when the incoming request does not contain a transaction id. */
097  public static final String DEFAULT_TRANSACTION_ID = "0";
098
099  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
100
101  private static final String AUDIT_SERVICE_JSON_CONFIGURATION_FILE = "audit-config.json";
102
103  /** Dependency provider used to instantiate the handlers. */
104  private final DependencyProvider dependencyProvider;
105
106  /** Configuration framework is used to get an up-to-date class loader with any external library available. */
107  private final ConfigurationFramework configurationFramework;
108
109  /** Cache of audit services per configuration entry normalized name. */
110  private final Map<String, AuditServiceProxy> auditServiceCache = new ConcurrentHashMap<>(10);
111
112  /** Cache of PublisherConfig per http access configuration entry normalized name. */
113  private final Map<String, PublisherConfig> httpAccessPublishers = new ConcurrentHashMap<>(5);
114
115  /** Cache of PublisherConfig per access configuration entry normalized name. */
116  private final Map<String, PublisherConfig> accessPublishers = new ConcurrentHashMap<>(5);
117
118  /** Audit service shared by all HTTP access publishers. */
119  private final AuditServiceProxy httpAccessAuditService;
120
121  private final AtomicBoolean trustTransactionIds = new AtomicBoolean(false);
122
123  /**
124   * Creates the common audit.
125   *
126   * @throws ConfigException
127   *           If an error occurs.
128   */
129  public CommonAudit() throws ConfigException
130  {
131    configurationFramework = ConfigurationFramework.getInstance();
132    this.dependencyProvider = new CommonAuditDependencyProvider();
133    this.httpAccessAuditService = createAuditServiceWithoutHandlers();
134  }
135
136  /**
137   * Indicates if transactionIds received from requests should be trusted.
138   *
139   * @return {@code true} if transactionIds should be trusted, {@code false} otherwise
140   */
141  public boolean shouldTrustTransactionIds()
142  {
143    return trustTransactionIds.get();
144  }
145
146  /**
147   * Sets the indicator for transactionIds trusting.
148   *
149   * @param shouldTrust
150   *          {@code true} if transactionIds should be trusted, {@code false}
151   *          otherwise
152   */
153  public void setTrustTransactionIds(boolean shouldTrust)
154  {
155    trustTransactionIds.set(shouldTrust);
156  }
157
158  private AuditServiceProxy createAuditServiceWithoutHandlers() throws ConfigException
159  {
160    try
161    {
162      return buildAuditService(new AuditServiceSetup()
163      {
164        @Override
165        public void addHandlers(AuditServiceBuilder builder)
166        {
167          // no handler to add
168        }
169      });
170    }
171    catch (IOException | ConfigException | AuditException e)
172    {
173      throw new ConfigException(ERR_COMMON_AUDIT_CREATE.get(e), e);
174    }
175  }
176
177  /**
178   * Returns the Common Audit request handler for the provided configuration.
179   *
180   * @param config
181   *            The log publisher configuration
182   * @return the request handler associated to the log publisher
183   * @throws ConfigException
184   *            If an error occurs
185   */
186  public RequestHandler getRequestHandler(LogPublisherCfg config) throws ConfigException
187  {
188    if (new PublisherConfig(config).isHttpAccessLog())
189    {
190      return httpAccessAuditService;
191    }
192    return auditServiceCache.get(getConfigNormalizedName(config));
193  }
194
195  /**
196   * Adds or updates the publisher corresponding to the provided configuration to common audit.
197   *
198   * @param newConfig
199   *          Configuration of the publisher
200   * @throws ConfigException
201   *           If an error occurs.
202   */
203  public void addOrUpdatePublisher(final LogPublisherCfg newConfig) throws ConfigException
204  {
205    if (newConfig.isEnabled())
206    {
207      logger.trace(String.format("Setting up common audit for configuration entry: %s", newConfig.dn()));
208      try
209      {
210        final PublisherConfig newPublisher = new PublisherConfig(newConfig);
211        String normalizedName = getConfigNormalizedName(newConfig);
212        if (newPublisher.isHttpAccessLog())
213        {
214          // if an old version exists, it is replaced by the new one
215          httpAccessPublishers.put(normalizedName, newPublisher);
216          buildAuditService(httpAccessAuditServiceSetup());
217        }
218        else // all other logs
219        {
220          final AuditServiceProxy existingService = auditServiceCache.get(normalizedName);
221          AuditServiceProxy auditService = buildAuditService(new AuditServiceSetup(existingService)
222          {
223            @Override
224            public void addHandlers(AuditServiceBuilder builder) throws ConfigException
225            {
226              registerHandlerName(newPublisher.getName());
227              addHandlerToBuilder(newPublisher, builder);
228            }
229          });
230          auditServiceCache.put(normalizedName, auditService);
231          accessPublishers.put(normalizedName, newPublisher);
232        }
233      }
234      catch (Exception e)
235      {
236        throw new ConfigException(ERR_COMMON_AUDIT_ADD_OR_UPDATE_LOG_PUBLISHER.get(newConfig.dn(), e), e);
237      }
238    }
239  }
240
241  /**
242   * Removes the publisher corresponding to the provided configuration from common audit.
243   *
244   * @param config
245   *          Configuration of publisher to remove
246   * @throws ConfigException
247   *            If an error occurs.
248   */
249  public void removePublisher(LogPublisherCfg config) throws ConfigException
250  {
251    logger.trace(String.format("Shutting down common audit for configuration entry:", config.dn()));
252    String normalizedName = getConfigNormalizedName(config);
253    try
254    {
255      if (httpAccessPublishers.containsKey(normalizedName))
256      {
257        httpAccessPublishers.remove(normalizedName);
258        buildAuditService(httpAccessAuditServiceSetup());
259      }
260      else if (accessPublishers.containsKey(normalizedName))
261      {
262        accessPublishers.remove(normalizedName);
263        AuditServiceProxy auditService = auditServiceCache.remove(normalizedName);
264        if (auditService != null)
265        {
266          auditService.shutdown();
267        }
268      }
269      // else it is not a registered publisher, nothing to do
270    }
271    catch (Exception e)
272    {
273      throw new ConfigException(ERR_COMMON_AUDIT_REMOVE_LOG_PUBLISHER.get(config.dn(), e), e);
274    }
275  }
276
277  /** Shutdown common audit. */
278  public void shutdown()
279  {
280    httpAccessAuditService.shutdown();
281    for (AuditServiceProxy service : auditServiceCache.values())
282    {
283      service.shutdown();
284    }
285  }
286
287  private AuditServiceSetup httpAccessAuditServiceSetup()
288  {
289    return new AuditServiceSetup(httpAccessAuditService)
290    {
291      @Override
292      public void addHandlers(AuditServiceBuilder builder) throws ConfigException
293      {
294        for (PublisherConfig publisher : httpAccessPublishers.values())
295        {
296          registerHandlerName(publisher.getName());
297          addHandlerToBuilder(publisher, builder);
298        }
299      }
300    };
301  }
302
303  /**
304   * Strategy for the setup of AuditService.
305   * <p>
306   * Unless no handler must be added, this class should be extended and
307   * implementations should override the {@code addHandlers()} method.
308   */
309  static abstract class AuditServiceSetup
310  {
311    private final AuditServiceProxy existingAuditServiceProxy;
312    private final List<String> names = new ArrayList<>();
313
314    /** Creation with no existing audit service. */
315    AuditServiceSetup()
316    {
317      this.existingAuditServiceProxy = null;
318    }
319
320    /** Creation with an existing audit service. */
321    AuditServiceSetup(AuditServiceProxy existingAuditService)
322    {
323      this.existingAuditServiceProxy = existingAuditService;
324    }
325
326    abstract void addHandlers(AuditServiceBuilder builder) throws ConfigException;
327
328    void registerHandlerName(String name)
329    {
330      names.add(name);
331    }
332
333    List<String> getHandlerNames()
334    {
335      return names;
336    }
337
338    boolean mustCreateAuditServiceProxy()
339    {
340      return existingAuditServiceProxy == null;
341    }
342
343    AuditServiceProxy getExistingAuditServiceProxy()
344    {
345      return existingAuditServiceProxy;
346    }
347
348  }
349
350  private AuditServiceProxy buildAuditService(AuditServiceSetup setup)
351      throws IOException, AuditException, ConfigException
352  {
353    final JsonValue jsonConfig;
354    try (InputStream input = getClass().getResourceAsStream(AUDIT_SERVICE_JSON_CONFIGURATION_FILE))
355    {
356      jsonConfig = AuditJsonConfig.getJson(input);
357    }
358
359    EventTopicsMetaData eventTopicsMetaData = coreTopicSchemas()
360        .withCoreTopicSchemaExtensions(jsonConfig.get("extensions"))
361        .withAdditionalTopicSchemas(jsonConfig.get("additionalTopics"))
362        .build();
363    AuditServiceBuilder builder = newAuditService()
364        .withEventTopicsMetaData(eventTopicsMetaData)
365        .withDependencyProvider(dependencyProvider);
366
367    setup.addHandlers(builder);
368
369    AuditServiceConfiguration auditConfig = new AuditServiceConfiguration();
370    auditConfig.setAvailableAuditEventHandlers(setup.getHandlerNames());
371    auditConfig.setFilterPolicies(getFilterPoliciesToPreventHttpHeadersLogging());
372    builder.withConfiguration(auditConfig);
373    AuditService audit = builder.build();
374
375    final AuditServiceProxy proxy;
376    if (setup.mustCreateAuditServiceProxy())
377    {
378      proxy = new AuditServiceProxy(audit);
379      logger.trace("Starting up new common audit service");
380      proxy.startup();
381    }
382    else
383    {
384      proxy = setup.getExistingAuditServiceProxy();
385      proxy.setDelegate(audit);
386      logger.trace("Starting up existing updated common audit service");
387    }
388    return proxy;
389  }
390
391  /**
392   * Build filter policies at the AuditService level to prevent logging of the headers for HTTP requests.
393   * <p>
394   * HTTP Headers may contains authentication information.
395   */
396  private Map<String, FilterPolicy> getFilterPoliciesToPreventHttpHeadersLogging()
397  {
398    Map<String, FilterPolicy> filterPolicies = new HashMap<>();
399    FilterPolicy policy = new FilterPolicy();
400    policy.setExcludeIf(asList("/http-access/http/request/headers"));
401    filterPolicies.put("field", policy);
402    return filterPolicies;
403  }
404
405  private void addHandlerToBuilder(PublisherConfig publisher, AuditServiceBuilder builder) throws ConfigException
406  {
407    if (publisher.isCsv())
408    {
409      addCsvHandler(publisher, builder);
410    }
411    else if (publisher.isExternal())
412    {
413      addExternalHandler(publisher, builder);
414    }
415    else
416    {
417      throw new ConfigException(ERR_COMMON_AUDIT_UNSUPPORTED_HANDLER_TYPE.get(publisher.getDn()));
418    }
419  }
420
421  /** Add a handler defined externally in a JSON configuration file. */
422  private void addExternalHandler(PublisherConfig publisher, AuditServiceBuilder builder) throws ConfigException
423  {
424    ExternalConfigData config = publisher.getExternalConfig();
425    File configFile = getFileForPath(config.getConfigurationFile());
426    try (InputStream input = new BufferedInputStream(new FileInputStream(configFile)))
427    {
428      JsonValue jsonConfig = AuditJsonConfig.getJson(input);
429      registerHandlerToService(jsonConfig, builder, configurationFramework.getClassLoader());
430    }
431    catch (IOException e)
432    {
433      throw new ConfigException(ERR_COMMON_AUDIT_EXTERNAL_HANDLER_JSON_FILE.get(configFile, publisher.getDn(), e), e);
434    }
435    catch (Exception e)
436    {
437      throw new ConfigException(ERR_COMMON_AUDIT_EXTERNAL_HANDLER_CREATION.get(publisher.getDn(), e), e);
438    }
439  }
440
441  private void addCsvHandler(PublisherConfig publisher, AuditServiceBuilder builder) throws ConfigException
442  {
443    String name = publisher.getName();
444    try
445    {
446      CsvConfigData config = publisher.getCsvConfig();
447      CsvAuditEventHandlerConfiguration csvConfig = new CsvAuditEventHandlerConfiguration();
448      File logDirectory = getFileForPath(config.getLogDirectory());
449      csvConfig.setLogDirectory(logDirectory.getAbsolutePath());
450      csvConfig.setName(name);
451      csvConfig.setTopics(Collections.singleton(publisher.getCommonAuditTopic()));
452
453      addCsvHandlerFormattingConfig(config, csvConfig);
454      addCsvHandlerBufferingConfig(config, csvConfig);
455      addCsvHandlerSecureConfig(publisher, config, csvConfig);
456      addCsvHandlerRotationConfig(publisher, config, csvConfig);
457      addCsvHandlerRetentionConfig(publisher, config, csvConfig);
458
459      builder.withAuditEventHandler(CsvAuditEventHandler.class, csvConfig);
460    }
461    catch (Exception e)
462    {
463      throw new ConfigException(ERR_COMMON_AUDIT_CSV_HANDLER_CREATION.get(publisher.getDn(), e), e);
464    }
465  }
466
467  private void addCsvHandlerFormattingConfig(CsvConfigData config, CsvAuditEventHandlerConfiguration auditConfig)
468      throws ConfigException
469  {
470    CsvFormatting formatting = new CsvFormatting();
471    formatting.setQuoteChar(config.getQuoteChar());
472    formatting.setDelimiterChar(config.getDelimiterChar());
473    String endOfLineSymbols = config.getEndOfLineSymbols();
474    if (endOfLineSymbols != null && !endOfLineSymbols.isEmpty())
475    {
476      formatting.setEndOfLineSymbols(endOfLineSymbols);
477    }
478    auditConfig.setFormatting(formatting);
479  }
480
481  private void addCsvHandlerBufferingConfig(CsvConfigData config, CsvAuditEventHandlerConfiguration auditConfig)
482  {
483    EventBufferingConfiguration bufferingConfig = new EventBufferingConfiguration();
484    bufferingConfig.setEnabled(config.isAsynchronous());
485    bufferingConfig.setAutoFlush(config.isAutoFlush());
486    auditConfig.setBufferingConfiguration(bufferingConfig);
487  }
488
489  private void addCsvHandlerSecureConfig(PublisherConfig publisher, CsvConfigData config,
490      CsvAuditEventHandlerConfiguration auditConfig)
491  {
492    if (config.isTamperEvident())
493    {
494      CsvSecurity security = new CsvSecurity();
495      security.setSignatureInterval(config.getSignatureTimeInterval() + "ms");
496      security.setEnabled(true);
497      String keyStoreFile = config.getKeystoreFile();
498      security.setFilename(getFileForPath(keyStoreFile).getPath());
499      security.setPassword(getSecurePassword(publisher, config));
500      auditConfig.setSecurity(security);
501    }
502  }
503
504  private void addCsvHandlerRotationConfig(PublisherConfig publisher, CsvConfigData config,
505      CsvAuditEventHandlerConfiguration auditConfig) throws ConfigException
506  {
507    RootCfg root = ServerManagementContext.getInstance().getRootConfiguration();
508    SortedSet<String> rotationPolicies = config.getRotationPolicies();
509    if (rotationPolicies.isEmpty())
510    {
511      return;
512    }
513
514    FileRotation fileRotation = new FileRotation();
515    fileRotation.setRotationEnabled(true);
516    for (final String policy : rotationPolicies)
517    {
518      LogRotationPolicyCfg policyConfig = root.getLogRotationPolicy(policy);
519      if (policyConfig instanceof FixedTimeLogRotationPolicyCfg)
520      {
521        List<String> times = convertTimesOfDay(publisher, (FixedTimeLogRotationPolicyCfg) policyConfig);
522        fileRotation.setRotationTimes(times);
523      }
524      else if (policyConfig instanceof SizeLimitLogRotationPolicyCfg)
525      {
526        fileRotation.setMaxFileSize(((SizeLimitLogRotationPolicyCfg) policyConfig).getFileSizeLimit());
527      }
528      else if (policyConfig instanceof TimeLimitLogRotationPolicyCfg)
529      {
530        long rotationInterval = ((TimeLimitLogRotationPolicyCfg) policyConfig).getRotationInterval();
531        fileRotation.setRotationInterval(String.valueOf(rotationInterval) + " ms");
532      }
533      else
534      {
535        throw new ConfigException(
536            ERR_COMMON_AUDIT_UNSUPPORTED_LOG_ROTATION_POLICY.get(publisher.getDn(), policyConfig.dn()));
537      }
538    }
539    auditConfig.setFileRotation(fileRotation);
540  }
541
542  private void addCsvHandlerRetentionConfig(PublisherConfig publisher, CsvConfigData config,
543      CsvAuditEventHandlerConfiguration auditConfig) throws ConfigException
544  {
545    RootCfg root = ServerManagementContext.getInstance().getRootConfiguration();
546    SortedSet<String> retentionPolicies = config.getRetentionPolicies();
547    if (retentionPolicies.isEmpty())
548    {
549      return;
550    }
551
552    FileRetention fileRetention = new FileRetention();
553    for (final String policy : retentionPolicies)
554    {
555      LogRetentionPolicyCfg policyConfig = root.getLogRetentionPolicy(policy);
556      if (policyConfig instanceof FileCountLogRetentionPolicyCfg)
557      {
558        fileRetention.setMaxNumberOfHistoryFiles(((FileCountLogRetentionPolicyCfg) policyConfig).getNumberOfFiles());
559      }
560      else if (policyConfig instanceof FreeDiskSpaceLogRetentionPolicyCfg)
561      {
562        fileRetention.setMinFreeSpaceRequired(((FreeDiskSpaceLogRetentionPolicyCfg) policyConfig).getFreeDiskSpace());
563      }
564      else if (policyConfig instanceof SizeLimitLogRetentionPolicyCfg)
565      {
566        fileRetention.setMaxDiskSpaceToUse(((SizeLimitLogRetentionPolicyCfg) policyConfig).getDiskSpaceUsed());
567      }
568      else
569      {
570        throw new ConfigException(
571            ERR_COMMON_AUDIT_UNSUPPORTED_LOG_RETENTION_POLICY.get(publisher.getDn(), policyConfig.dn()));
572      }
573    }
574    auditConfig.setFileRetention(fileRetention);
575  }
576
577  /**
578   * Convert the set of provided times of day using 24-hour format "HHmm" to a list of
579   * times of day using duration in minutes, e.g "20 minutes".
580   * <p>
581   * Example: "0230" => "150 minutes"
582   */
583  private List<String> convertTimesOfDay(PublisherConfig publisher, FixedTimeLogRotationPolicyCfg policyConfig)
584      throws ConfigException
585  {
586    SortedSet<String> timesOfDay = policyConfig.getTimeOfDay();
587    List<String> times = new ArrayList<>();
588    for (String timeOfDay : timesOfDay)
589    {
590      try
591      {
592        int time = Integer.valueOf(timeOfDay.substring(0, 2)) * 60 + Integer.valueOf(timeOfDay.substring(2, 4));
593        times.add(String.valueOf(time) + " minutes");
594      }
595      catch (NumberFormatException | IndexOutOfBoundsException e)
596      {
597        throw new ConfigException(ERR_COMMON_AUDIT_INVALID_TIME_OF_DAY.get(publisher.getDn(), timeOfDay,
598            StaticUtils.stackTraceToSingleLineString(e)));
599      }
600    }
601    return times;
602  }
603
604  private String getSecurePassword(PublisherConfig publisher, CsvConfigData config)
605  {
606    String fileName = config.getKeystorePinFile();
607    File pinFile = getFileForPath(fileName);
608
609    if (!pinFile.exists())
610    {
611      logger.warn(ERR_COMMON_AUDIT_KEYSTORE_PIN_FILE_MISSING.get(publisher.getDn(), pinFile));
612      return "";
613    }
614
615    try (BufferedReader br = new BufferedReader(new FileReader(pinFile)))
616    {
617      String pinStr = br.readLine();
618      if (pinStr == null)
619      {
620        logger.warn(ERR_COMMON_AUDIT_KEYSTORE_PIN_FILE_CONTAINS_EMPTY_PIN.get(publisher.getDn(), pinFile));
621        return "";
622      }
623      return pinStr;
624    }
625    catch (IOException ioe)
626    {
627      logger.warn(ERR_COMMON_AUDIT_ERROR_READING_KEYSTORE_PIN_FILE.get(publisher.getDn(), pinFile,
628          stackTraceToSingleLineString(ioe)), ioe);
629      return "";
630    }
631  }
632
633  /**
634   * Indicates if the provided log publisher configuration corresponds to a common audit publisher.
635   * <p>
636   * The common audit publisher may not already exist.
637   * <p>
638   * This method must not be used when the corresponding configuration is deleted, because it
639   * implies checking the corresponding configuration entry in the server.
640   *
641   * @param config
642   *          The log publisher configuration.
643   * @return {@code true} if publisher corresponds to a common audit publisher
644   * @throws ConfigException
645   *           If an error occurs
646   */
647  public boolean isCommonAuditConfig(LogPublisherCfg config) throws ConfigException
648  {
649    return new PublisherConfig(config).isCommonAudit();
650  }
651
652  /**
653   * Indicates if the provided log publisher configuration corresponds to a common audit publisher.
654   *
655   * @param config
656   *          The log publisher configuration.
657   * @return {@code true} if publisher is defined for common audit, {@code false} otherwise
658   * @throws ConfigException
659   *           If an error occurs
660   */
661  public boolean isExistingCommonAuditConfig(LogPublisherCfg config) throws ConfigException
662  {
663    String name = getConfigNormalizedName(config);
664    return accessPublishers.containsKey(name) || httpAccessPublishers.containsKey(name);
665  }
666
667  /**
668   * Indicates if HTTP access logging is enabled for common audit.
669   *
670   * @return {@code true} if there is at least one HTTP access logger enabled for common audit.
671   */
672  public boolean isHttpAccessLogEnabled()
673  {
674    return !httpAccessPublishers.isEmpty();
675  }
676
677  private String getConfigNormalizedName(LogPublisherCfg config)
678  {
679    return config.dn().toNormalizedUrlSafeString();
680  }
681
682  /**
683   * Returns the audit service that manages HTTP Access logging.
684   *
685   * @return the request handler that accepts audit events
686   */
687  public RequestHandler getAuditServiceForHttpAccessLog()
688  {
689    return httpAccessAuditService;
690  }
691
692  /**
693   * This class hides all ugly code needed to determine which type of publisher and audit event handler is needed.
694   * <p>
695   * In particular, it allows to retrieve a common configuration that can be used for log publishers that
696   * publish to the same kind of handler.
697   * For example: for CSV handler, DJ configurations for the log publishers contain the same methods but
698   * do not have a common interface (CsvFileAccessLogPublisherCfg vs CsvFileHTTPAccessLogPublisherCfg).
699   */
700  private static class PublisherConfig
701  {
702    private final LogPublisherCfg config;
703    private final boolean isCommonAudit;
704    private LogType logType;
705    private AuditType auditType;
706
707    PublisherConfig(LogPublisherCfg config) throws ConfigException
708    {
709      this.config = config;
710      ConfigEntry configEntry = DirectoryServer.getConfigEntry(config.dn());
711      if (configEntry.hasObjectClass("ds-cfg-csv-file-access-log-publisher"))
712      {
713        auditType = AuditType.CSV;
714        logType = LogType.ACCESS;
715      }
716      else if (configEntry.hasObjectClass("ds-cfg-csv-file-http-access-log-publisher"))
717      {
718        auditType = AuditType.CSV;
719        logType = LogType.HTTP_ACCESS;
720      }
721      else if (configEntry.hasObjectClass("ds-cfg-external-access-log-publisher"))
722      {
723        auditType = AuditType.EXTERNAL;
724        logType = LogType.ACCESS;
725      }
726      else if (configEntry.hasObjectClass("ds-cfg-external-http-access-log-publisher"))
727      {
728        auditType = AuditType.EXTERNAL;
729        logType = LogType.HTTP_ACCESS;
730      }
731      isCommonAudit = auditType != null;
732    }
733
734    DN getDn()
735    {
736      return config.dn();
737    }
738
739    String getName()
740    {
741      return config.dn().rdn().getFirstAVA().getAttributeValue().toString();
742    }
743
744    String getCommonAuditTopic() throws ConfigException
745    {
746      if (isAccessLog())
747      {
748        return "ldap-access";
749      }
750      else if (isHttpAccessLog())
751      {
752        return "http-access";
753      }
754      throw new ConfigException(ERR_COMMON_AUDIT_UNSUPPORTED_LOG_PUBLISHER.get(config.dn()));
755    }
756
757    boolean isExternal()
758    {
759      return AuditType.EXTERNAL == auditType;
760    }
761
762    boolean isCsv()
763    {
764      return AuditType.CSV == auditType;
765    }
766
767    boolean isAccessLog()
768    {
769      return LogType.ACCESS == logType;
770    }
771
772    boolean isHttpAccessLog()
773    {
774      return LogType.HTTP_ACCESS == logType;
775    }
776
777    boolean isCommonAudit()
778    {
779      return isCommonAudit;
780    }
781
782    CsvConfigData getCsvConfig() throws ConfigException
783    {
784      if (isAccessLog())
785      {
786        CsvFileAccessLogPublisherCfg conf = (CsvFileAccessLogPublisherCfg) config;
787        return new CsvConfigData(conf.getLogDirectory(), conf.getCsvQuoteChar(), conf.getCsvDelimiterChar(), conf
788            .getCsvEolSymbols(), conf.isAsynchronous(), conf.isAutoFlush(), conf.isTamperEvident(), conf
789            .getSignatureTimeInterval(), conf.getKeyStoreFile(), conf.getKeyStorePinFile(), conf.getRotationPolicy(),
790            conf.getRetentionPolicy());
791      }
792      if (isHttpAccessLog())
793      {
794        CsvFileHTTPAccessLogPublisherCfg conf = (CsvFileHTTPAccessLogPublisherCfg) config;
795        return new CsvConfigData(conf.getLogDirectory(), conf.getCsvQuoteChar(), conf.getCsvDelimiterChar(), conf
796            .getCsvEolSymbols(), conf.isAsynchronous(), conf.isAutoFlush(), conf.isTamperEvident(), conf
797            .getSignatureTimeInterval(), conf.getKeyStoreFile(), conf.getKeyStorePinFile(), conf.getRotationPolicy(),
798            conf.getRetentionPolicy());
799      }
800      throw new ConfigException(ERR_COMMON_AUDIT_UNSUPPORTED_LOG_PUBLISHER.get(config.dn()));
801    }
802
803    ExternalConfigData getExternalConfig() throws ConfigException
804    {
805      if (isAccessLog())
806      {
807        ExternalAccessLogPublisherCfg conf = (ExternalAccessLogPublisherCfg) config;
808        return new ExternalConfigData(conf.getConfigFile());
809      }
810      if (isHttpAccessLog())
811      {
812        ExternalHTTPAccessLogPublisherCfg conf = (ExternalHTTPAccessLogPublisherCfg) config;
813        return new ExternalConfigData(conf.getConfigFile());
814      }
815      throw new ConfigException(ERR_COMMON_AUDIT_UNSUPPORTED_LOG_PUBLISHER.get(config.dn()));
816    }
817
818    @Override
819    public boolean equals(Object obj)
820    {
821      if (this == obj)
822      {
823        return true;
824      }
825      if (!(obj instanceof PublisherConfig))
826      {
827        return false;
828      }
829      PublisherConfig other = (PublisherConfig) obj;
830      return config.dn().equals(other.config.dn());
831    }
832
833    @Override
834    public int hashCode()
835    {
836      return config.dn().hashCode();
837    }
838
839  }
840
841  /** Types of audit handlers managed. */
842  private enum AuditType
843  {
844    CSV, EXTERNAL
845  }
846
847  /** Types of log managed. */
848  private enum LogType
849  {
850    ACCESS, HTTP_ACCESS
851  }
852
853  /**
854   * Contains the parameters for a CSV handler.
855   * <p>
856   * OpenDJ log publishers that logs to a CSV handler have the same parameters but do not share
857   * a common ancestor with all the parameters (e.g Access Log, HTTP Access Log, ...), hence this class
858   * is necessary to avoid duplicating code that setup the configuration of the CSV handler.
859   */
860  private static class CsvConfigData
861  {
862    private final String logDirectory;
863    private final String eolSymbols;
864    private final String delimiterChar;
865    private final String quoteChar;
866    private final boolean asynchronous;
867    private final boolean autoFlush;
868    private final boolean tamperEvident;
869    private final long signatureTimeInterval;
870    private final String keystoreFile;
871    private final String keystorePinFile;
872    private final SortedSet<String> rotationPolicies;
873    private final SortedSet<String> retentionPolicies;
874
875    CsvConfigData(String logDirectory, String quoteChar, String delimiterChar, String eolSymbols, boolean asynchronous,
876        boolean autoFlush, boolean tamperEvident, long signatureTimeInterval, String keystoreFile,
877        String keystorePinFile, SortedSet<String> rotationPolicies, SortedSet<String> retentionPolicies)
878    {
879      this.logDirectory = logDirectory;
880      this.quoteChar = quoteChar;
881      this.delimiterChar = delimiterChar;
882      this.eolSymbols = eolSymbols;
883      this.asynchronous = asynchronous;
884      this.autoFlush = autoFlush;
885      this.tamperEvident = tamperEvident;
886      this.signatureTimeInterval = signatureTimeInterval;
887      this.keystoreFile = keystoreFile;
888      this.keystorePinFile = keystorePinFile;
889      this.rotationPolicies = rotationPolicies;
890      this.retentionPolicies = retentionPolicies;
891    }
892
893    String getEndOfLineSymbols()
894    {
895      return eolSymbols;
896    }
897
898    char getDelimiterChar() throws ConfigException
899    {
900      String filtered = delimiterChar.replaceAll(Pattern.quote("\\"), "");
901      if (filtered.length() != 1)
902      {
903        throw new ConfigException(ERR_COMMON_AUDIT_CSV_HANDLER_DELIMITER_CHAR.get("", filtered));
904      }
905      return filtered.charAt(0);
906    }
907
908    public char getQuoteChar() throws ConfigException
909    {
910      String filtered = quoteChar.replaceAll(Pattern.quote("\\"), "");
911      if (filtered.length() != 1)
912      {
913        throw new ConfigException(ERR_COMMON_AUDIT_CSV_HANDLER_QUOTE_CHAR.get("", filtered));
914      }
915      return filtered.charAt(0);
916    }
917
918    String getLogDirectory()
919    {
920      return logDirectory;
921    }
922
923    boolean isAsynchronous()
924    {
925      return asynchronous;
926    }
927
928    boolean isAutoFlush()
929    {
930      return autoFlush;
931    }
932
933    boolean isTamperEvident()
934    {
935      return tamperEvident;
936    }
937
938    long getSignatureTimeInterval()
939    {
940      return signatureTimeInterval;
941    }
942
943    String getKeystoreFile()
944    {
945      return keystoreFile;
946    }
947
948    String getKeystorePinFile()
949    {
950      return keystorePinFile;
951    }
952
953    SortedSet<String> getRotationPolicies()
954    {
955      return rotationPolicies;
956    }
957
958    SortedSet<String> getRetentionPolicies()
959    {
960      return retentionPolicies;
961    }
962  }
963
964  /**
965   * Contains the parameters for an external handler.
966   * <p>
967   * OpenDJ log publishers that logs to an external handler have the same
968   * parameters but do not share a common ancestor with all the parameters (e.g
969   * Access Log, HTTP Access Log, ...), hence this class is necessary to avoid
970   * duplicating code that setup the configuration of an external handler.
971   */
972  private static class ExternalConfigData
973  {
974    private final String configurationFile;
975
976    ExternalConfigData(String configurationFile)
977    {
978      this.configurationFile = configurationFile;
979    }
980
981    String getConfigurationFile()
982    {
983      return configurationFile;
984    }
985  }
986
987}