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