001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2015 ForgeRock AS.
025 */
026package org.opends.server.loggers;
027
028import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
029
030import static java.util.Arrays.asList;
031import static org.opends.messages.LoggerMessages.*;
032import static org.forgerock.audit.AuditServiceBuilder.newAuditService;
033import static org.forgerock.audit.events.EventTopicsMetaDataBuilder.coreTopicSchemas;
034import static org.forgerock.audit.json.AuditJsonConfig.registerHandlerToService;
035import static org.opends.server.util.StaticUtils.getFileForPath;
036
037import java.io.BufferedInputStream;
038import java.io.BufferedReader;
039import java.io.File;
040import java.io.FileInputStream;
041import java.io.FileReader;
042import java.io.IOException;
043import java.io.InputStream;
044import java.util.ArrayList;
045import java.util.Collections;
046import java.util.HashMap;
047import java.util.List;
048import java.util.Map;
049import java.util.SortedSet;
050import java.util.concurrent.ConcurrentHashMap;
051import java.util.concurrent.atomic.AtomicBoolean;
052import java.util.regex.Pattern;
053
054import org.forgerock.audit.AuditException;
055import org.forgerock.audit.AuditService;
056import org.forgerock.audit.AuditServiceBuilder;
057import org.forgerock.audit.AuditServiceConfiguration;
058import org.forgerock.audit.AuditServiceProxy;
059import org.forgerock.audit.DependencyProvider;
060import org.forgerock.audit.events.EventTopicsMetaData;
061import org.forgerock.audit.events.handlers.FileBasedEventHandlerConfiguration.FileRetention;
062import org.forgerock.audit.events.handlers.FileBasedEventHandlerConfiguration.FileRotation;
063import org.forgerock.audit.filter.FilterPolicy;
064import org.forgerock.audit.handlers.csv.CsvAuditEventHandler;
065import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration;
066import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration.CsvFormatting;
067import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration.CsvSecurity;
068import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration.EventBufferingConfiguration;
069import org.forgerock.audit.json.AuditJsonConfig;
070import org.forgerock.i18n.slf4j.LocalizedLogger;
071import org.forgerock.json.JsonValue;
072import org.forgerock.json.resource.RequestHandler;
073import org.forgerock.opendj.config.ConfigurationFramework;
074import org.forgerock.opendj.config.server.ConfigException;
075import org.opends.server.admin.server.ServerManagementContext;
076import org.opends.server.admin.std.server.CsvFileAccessLogPublisherCfg;
077import org.opends.server.admin.std.server.CsvFileHTTPAccessLogPublisherCfg;
078import org.opends.server.admin.std.server.ExternalAccessLogPublisherCfg;
079import org.opends.server.admin.std.server.ExternalHTTPAccessLogPublisherCfg;
080import org.opends.server.admin.std.server.FileCountLogRetentionPolicyCfg;
081import org.opends.server.admin.std.server.FixedTimeLogRotationPolicyCfg;
082import org.opends.server.admin.std.server.FreeDiskSpaceLogRetentionPolicyCfg;
083import org.opends.server.admin.std.server.LogPublisherCfg;
084import org.opends.server.admin.std.server.LogRetentionPolicyCfg;
085import org.opends.server.admin.std.server.LogRotationPolicyCfg;
086import org.opends.server.admin.std.server.RootCfg;
087import org.opends.server.admin.std.server.SizeLimitLogRetentionPolicyCfg;
088import org.opends.server.admin.std.server.SizeLimitLogRotationPolicyCfg;
089import org.opends.server.admin.std.server.TimeLimitLogRotationPolicyCfg;
090import org.opends.server.config.ConfigEntry;
091import org.opends.server.core.DirectoryServer;
092import org.opends.server.types.DN;
093import org.opends.server.util.StaticUtils;
094
095/**
096 * Entry point for the common audit facility.
097 * <p>
098 * This class manages the AuditService instances and Audit Event Handlers that correspond to the
099 * publishers defined in OpenDJ configuration.
100 * <p>
101 * In theory there should be only one instance of AuditService for all the event handlers but
102 * defining one service per handler allow to perform filtering at the DJ server level.
103 */
104public class CommonAudit
105{
106  /** Transaction id used when the incoming request does not contain a transaction id. */
107  public static final String DEFAULT_TRANSACTION_ID = "0";
108
109  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
110
111  private static final String AUDIT_SERVICE_JSON_CONFIGURATION_FILE = "audit-config.json";
112
113  /** Dependency provider used to instantiate the handlers. */
114  private final DependencyProvider dependencyProvider;
115
116  /** Configuration framework is used to get an up-to-date class loader with any external library available. */
117  private final ConfigurationFramework configurationFramework;
118
119  /** Cache of audit services per configuration entry normalized name. */
120  private final Map<String, AuditServiceProxy> auditServiceCache = new ConcurrentHashMap<>(10);
121
122  /** Cache of PublisherConfig per http access configuration entry normalized name. */
123  private final Map<String, PublisherConfig> httpAccessPublishers = new ConcurrentHashMap<>(5);
124
125  /** Cache of PublisherConfig per access configuration entry normalized name. */
126  private final Map<String, PublisherConfig> accessPublishers = new ConcurrentHashMap<>(5);
127
128  /** Audit service shared by all HTTP access publishers. */
129  private final AuditServiceProxy httpAccessAuditService;
130
131  private final AtomicBoolean trustTransactionIds = new AtomicBoolean(false);
132
133  /**
134   * Creates the common audit.
135   *
136   * @throws ConfigException
137   *           If an error occurs.
138   */
139  public CommonAudit() throws ConfigException
140  {
141    configurationFramework = ConfigurationFramework.getInstance();
142    this.dependencyProvider = new CommonAuditDependencyProvider();
143    this.httpAccessAuditService = createAuditServiceWithoutHandlers();
144  }
145
146  /**
147   * Indicates if transactionIds received from requests should be trusted.
148   *
149   * @return {@code true} if transactionIds should be trusted, {@code false} otherwise
150   */
151  public boolean shouldTrustTransactionIds()
152  {
153    return trustTransactionIds.get();
154  }
155
156  /**
157   * Sets the indicator for transactionIds trusting.
158   *
159   * @param shouldTrust
160   *          {@code true} if transactionIds should be trusted, {@code false}
161   *          otherwise
162   */
163  public void setTrustTransactionIds(boolean shouldTrust)
164  {
165    trustTransactionIds.set(shouldTrust);
166  }
167
168  private AuditServiceProxy createAuditServiceWithoutHandlers() throws ConfigException
169  {
170    try
171    {
172      return buildAuditService(new AuditServiceSetup()
173      {
174        @Override
175        public void addHandlers(AuditServiceBuilder builder)
176        {
177          // no handler to add
178        }
179      });
180    }
181    catch (IOException | ConfigException | AuditException e)
182    {
183      throw new ConfigException(ERR_COMMON_AUDIT_CREATE.get(e), e);
184    }
185  }
186
187  /**
188   * Returns the Common Audit request handler for the provided configuration.
189   *
190   * @param config
191   *            The log publisher configuration
192   * @return the request handler associated to the log publisher
193   * @throws ConfigException
194   *            If an error occurs
195   */
196  public RequestHandler getRequestHandler(LogPublisherCfg config) throws ConfigException
197  {
198    if (new PublisherConfig(config).isHttpAccessLog())
199    {
200      return httpAccessAuditService;
201    }
202    return auditServiceCache.get(getConfigNormalizedName(config));
203  }
204
205  /**
206   * Adds or updates the publisher corresponding to the provided configuration to common audit.
207   *
208   * @param newConfig
209   *          Configuration of the publisher
210   * @throws ConfigException
211   *           If an error occurs.
212   */
213  public void addOrUpdatePublisher(final LogPublisherCfg newConfig) throws ConfigException
214  {
215    if (newConfig.isEnabled())
216    {
217      logger.trace(String.format("Setting up common audit for configuration entry: %s", newConfig.dn()));
218      try
219      {
220        final PublisherConfig newPublisher = new PublisherConfig(newConfig);
221        String normalizedName = getConfigNormalizedName(newConfig);
222        if (newPublisher.isHttpAccessLog())
223        {
224          // if an old version exists, it is replaced by the new one
225          httpAccessPublishers.put(normalizedName, newPublisher);
226          buildAuditService(httpAccessAuditServiceSetup());
227        }
228        else // all other logs
229        {
230          final AuditServiceProxy existingService = auditServiceCache.get(normalizedName);
231          AuditServiceProxy auditService = buildAuditService(new AuditServiceSetup(existingService)
232          {
233            @Override
234            public void addHandlers(AuditServiceBuilder builder) throws ConfigException
235            {
236              registerHandlerName(newPublisher.getName());
237              addHandlerToBuilder(newPublisher, builder);
238            }
239          });
240          auditServiceCache.put(normalizedName, auditService);
241          accessPublishers.put(normalizedName, newPublisher);
242        }
243      }
244      catch (Exception e)
245      {
246        throw new ConfigException(ERR_COMMON_AUDIT_ADD_OR_UPDATE_LOG_PUBLISHER.get(newConfig.dn(), e), e);
247      }
248    }
249  }
250
251  /**
252   * Removes the publisher corresponding to the provided configuration from common audit.
253   *
254   * @param config
255   *          Configuration of publisher to remove
256   * @throws ConfigException
257   *            If an error occurs.
258   */
259  public void removePublisher(LogPublisherCfg config) throws ConfigException
260  {
261    logger.trace(String.format("Shutting down common audit for configuration entry:", config.dn()));
262    String normalizedName = getConfigNormalizedName(config);
263    try
264    {
265      if (httpAccessPublishers.containsKey(normalizedName))
266      {
267        httpAccessPublishers.remove(normalizedName);
268        buildAuditService(httpAccessAuditServiceSetup());
269      }
270      else if (accessPublishers.containsKey(normalizedName))
271      {
272        accessPublishers.remove(normalizedName);
273        AuditServiceProxy auditService = auditServiceCache.remove(normalizedName);
274        if (auditService != null)
275        {
276          auditService.shutdown();
277        }
278      }
279      // else it is not a registered publisher, nothing to do
280    }
281    catch (Exception e)
282    {
283      throw new ConfigException(ERR_COMMON_AUDIT_REMOVE_LOG_PUBLISHER.get(config.dn(), e), e);
284    }
285  }
286
287  /** Shutdown common audit. */
288  public void shutdown()
289  {
290    httpAccessAuditService.shutdown();
291    for (AuditServiceProxy service : auditServiceCache.values())
292    {
293      service.shutdown();
294    }
295  }
296
297  private AuditServiceSetup httpAccessAuditServiceSetup()
298  {
299    return new AuditServiceSetup(httpAccessAuditService)
300    {
301      @Override
302      public void addHandlers(AuditServiceBuilder builder) throws ConfigException
303      {
304        for (PublisherConfig publisher : httpAccessPublishers.values())
305        {
306          registerHandlerName(publisher.getName());
307          addHandlerToBuilder(publisher, builder);
308        }
309      }
310    };
311  }
312
313  /**
314   * Strategy for the setup of AuditService.
315   * <p>
316   * Unless no handler must be added, this class should be extended and
317   * implementations should override the {@code addHandlers()} method.
318   */
319  static abstract class AuditServiceSetup
320  {
321    private final AuditServiceProxy existingAuditServiceProxy;
322    private final List<String> names = new ArrayList<>();
323
324    /** Creation with no existing audit service. */
325    AuditServiceSetup()
326    {
327      this.existingAuditServiceProxy = null;
328    }
329
330    /** Creation with an existing audit service. */
331    AuditServiceSetup(AuditServiceProxy existingAuditService)
332    {
333      this.existingAuditServiceProxy = existingAuditService;
334    }
335
336    abstract void addHandlers(AuditServiceBuilder builder) throws ConfigException;
337
338    void registerHandlerName(String name)
339    {
340      names.add(name);
341    }
342
343    List<String> getHandlerNames()
344    {
345      return names;
346    }
347
348    boolean mustCreateAuditServiceProxy()
349    {
350      return existingAuditServiceProxy == null;
351    }
352
353    AuditServiceProxy getExistingAuditServiceProxy()
354    {
355      return existingAuditServiceProxy;
356    }
357
358  }
359
360  private AuditServiceProxy buildAuditService(AuditServiceSetup setup)
361      throws IOException, AuditException, ConfigException
362  {
363    final JsonValue jsonConfig;
364    try (InputStream input = getClass().getResourceAsStream(AUDIT_SERVICE_JSON_CONFIGURATION_FILE))
365    {
366      jsonConfig = AuditJsonConfig.getJson(input);
367    }
368
369    EventTopicsMetaData eventTopicsMetaData = coreTopicSchemas()
370        .withCoreTopicSchemaExtensions(jsonConfig.get("extensions"))
371        .withAdditionalTopicSchemas(jsonConfig.get("additionalTopics"))
372        .build();
373    AuditServiceBuilder builder = newAuditService()
374        .withEventTopicsMetaData(eventTopicsMetaData)
375        .withDependencyProvider(dependencyProvider);
376
377    setup.addHandlers(builder);
378
379    AuditServiceConfiguration auditConfig = new AuditServiceConfiguration();
380    auditConfig.setAvailableAuditEventHandlers(setup.getHandlerNames());
381    auditConfig.setFilterPolicies(getFilterPoliciesToPreventHttpHeadersLogging());
382    builder.withConfiguration(auditConfig);
383    AuditService audit = builder.build();
384
385    final AuditServiceProxy proxy;
386    if (setup.mustCreateAuditServiceProxy())
387    {
388      proxy = new AuditServiceProxy(audit);
389      logger.trace("Starting up new common audit service");
390      proxy.startup();
391    }
392    else
393    {
394      proxy = setup.getExistingAuditServiceProxy();
395      proxy.setDelegate(audit);
396      logger.trace("Starting up existing updated common audit service");
397    }
398    return proxy;
399  }
400
401  /**
402   * Build filter policies at the AuditService level to prevent logging of the headers for HTTP requests.
403   * <p>
404   * HTTP Headers may contains authentication information.
405   */
406  private Map<String, FilterPolicy> getFilterPoliciesToPreventHttpHeadersLogging()
407  {
408    Map<String, FilterPolicy> filterPolicies = new HashMap<>();
409    FilterPolicy policy = new FilterPolicy();
410    policy.setExcludeIf(asList("/http-access/http/request/headers"));
411    filterPolicies.put("field", policy);
412    return filterPolicies;
413  }
414
415  private void addHandlerToBuilder(PublisherConfig publisher, AuditServiceBuilder builder) throws ConfigException
416  {
417    if (publisher.isCsv())
418    {
419      addCsvHandler(publisher, builder);
420    }
421    else if (publisher.isExternal())
422    {
423      addExternalHandler(publisher, builder);
424    }
425    else
426    {
427      throw new ConfigException(ERR_COMMON_AUDIT_UNSUPPORTED_HANDLER_TYPE.get(publisher.getDn()));
428    }
429  }
430
431  /** Add a handler defined externally in a JSON configuration file. */
432  private void addExternalHandler(PublisherConfig publisher, AuditServiceBuilder builder) throws ConfigException
433  {
434    ExternalConfigData config = publisher.getExternalConfig();
435    File configFile = getFileForPath(config.getConfigurationFile());
436    try (InputStream input = new BufferedInputStream(new FileInputStream(configFile)))
437    {
438      JsonValue jsonConfig = AuditJsonConfig.getJson(input);
439      registerHandlerToService(jsonConfig, builder, configurationFramework.getClassLoader());
440    }
441    catch (IOException e)
442    {
443      throw new ConfigException(ERR_COMMON_AUDIT_EXTERNAL_HANDLER_JSON_FILE.get(configFile, publisher.getDn(), e), e);
444    }
445    catch (Exception e)
446    {
447      throw new ConfigException(ERR_COMMON_AUDIT_EXTERNAL_HANDLER_CREATION.get(publisher.getDn(), e), e);
448    }
449  }
450
451  private void addCsvHandler(PublisherConfig publisher, AuditServiceBuilder builder) throws ConfigException
452  {
453    String name = publisher.getName();
454    try
455    {
456      CsvConfigData config = publisher.getCsvConfig();
457      CsvAuditEventHandlerConfiguration csvConfig = new CsvAuditEventHandlerConfiguration();
458      File logDirectory = getFileForPath(config.getLogDirectory());
459      csvConfig.setLogDirectory(logDirectory.getAbsolutePath());
460      csvConfig.setName(name);
461      csvConfig.setTopics(Collections.singleton(publisher.getCommonAuditTopic()));
462
463      addCsvHandlerFormattingConfig(config, csvConfig);
464      addCsvHandlerBufferingConfig(config, csvConfig);
465      addCsvHandlerSecureConfig(publisher, config, csvConfig);
466      addCsvHandlerRotationConfig(publisher, config, csvConfig);
467      addCsvHandlerRetentionConfig(publisher, config, csvConfig);
468
469      builder.withAuditEventHandler(CsvAuditEventHandler.class, csvConfig);
470    }
471    catch (Exception e)
472    {
473      throw new ConfigException(ERR_COMMON_AUDIT_CSV_HANDLER_CREATION.get(publisher.getDn(), e), e);
474    }
475  }
476
477  private void addCsvHandlerFormattingConfig(CsvConfigData config, CsvAuditEventHandlerConfiguration auditConfig)
478      throws ConfigException
479  {
480    CsvFormatting formatting = new CsvFormatting();
481    formatting.setQuoteChar(config.getQuoteChar());
482    formatting.setDelimiterChar(config.getDelimiterChar());
483    String endOfLineSymbols = config.getEndOfLineSymbols();
484    if (endOfLineSymbols != null && !endOfLineSymbols.isEmpty())
485    {
486      formatting.setEndOfLineSymbols(endOfLineSymbols);
487    }
488    auditConfig.setFormatting(formatting);
489  }
490
491  private void addCsvHandlerBufferingConfig(CsvConfigData config, CsvAuditEventHandlerConfiguration auditConfig)
492  {
493    EventBufferingConfiguration bufferingConfig = new EventBufferingConfiguration();
494    bufferingConfig.setEnabled(config.isAsynchronous());
495    bufferingConfig.setAutoFlush(config.isAutoFlush());
496    auditConfig.setBufferingConfiguration(bufferingConfig);
497  }
498
499  private void addCsvHandlerSecureConfig(PublisherConfig publisher, CsvConfigData config,
500      CsvAuditEventHandlerConfiguration auditConfig)
501  {
502    if (config.isTamperEvident())
503    {
504      CsvSecurity security = new CsvSecurity();
505      security.setSignatureInterval(config.getSignatureTimeInterval() + "ms");
506      security.setEnabled(true);
507      String keyStoreFile = config.getKeystoreFile();
508      security.setFilename(getFileForPath(keyStoreFile).getPath());
509      security.setPassword(getSecurePassword(publisher, config));
510      auditConfig.setSecurity(security);
511    }
512  }
513
514  private void addCsvHandlerRotationConfig(PublisherConfig publisher, CsvConfigData config,
515      CsvAuditEventHandlerConfiguration auditConfig) throws ConfigException
516  {
517    RootCfg root = ServerManagementContext.getInstance().getRootConfiguration();
518    SortedSet<String> rotationPolicies = config.getRotationPolicies();
519    if (rotationPolicies.isEmpty())
520    {
521      return;
522    }
523
524    FileRotation fileRotation = new FileRotation();
525    fileRotation.setRotationEnabled(true);
526    for (final String policy : rotationPolicies)
527    {
528      LogRotationPolicyCfg policyConfig = root.getLogRotationPolicy(policy);
529      if (policyConfig instanceof FixedTimeLogRotationPolicyCfg)
530      {
531        List<String> times = convertTimesOfDay(publisher, (FixedTimeLogRotationPolicyCfg) policyConfig);
532        fileRotation.setRotationTimes(times);
533      }
534      else if (policyConfig instanceof SizeLimitLogRotationPolicyCfg)
535      {
536        fileRotation.setMaxFileSize(((SizeLimitLogRotationPolicyCfg) policyConfig).getFileSizeLimit());
537      }
538      else if (policyConfig instanceof TimeLimitLogRotationPolicyCfg)
539      {
540        long rotationInterval = ((TimeLimitLogRotationPolicyCfg) policyConfig).getRotationInterval();
541        fileRotation.setRotationInterval(String.valueOf(rotationInterval) + " ms");
542      }
543      else
544      {
545        throw new ConfigException(
546            ERR_COMMON_AUDIT_UNSUPPORTED_LOG_ROTATION_POLICY.get(publisher.getDn(), policyConfig.dn()));
547      }
548    }
549    auditConfig.setFileRotation(fileRotation);
550  }
551
552  private void addCsvHandlerRetentionConfig(PublisherConfig publisher, CsvConfigData config,
553      CsvAuditEventHandlerConfiguration auditConfig) throws ConfigException
554  {
555    RootCfg root = ServerManagementContext.getInstance().getRootConfiguration();
556    SortedSet<String> retentionPolicies = config.getRetentionPolicies();
557    if (retentionPolicies.isEmpty())
558    {
559      return;
560    }
561
562    FileRetention fileRetention = new FileRetention();
563    for (final String policy : retentionPolicies)
564    {
565      LogRetentionPolicyCfg policyConfig = root.getLogRetentionPolicy(policy);
566      if (policyConfig instanceof FileCountLogRetentionPolicyCfg)
567      {
568        fileRetention.setMaxNumberOfHistoryFiles(((FileCountLogRetentionPolicyCfg) policyConfig).getNumberOfFiles());
569      }
570      else if (policyConfig instanceof FreeDiskSpaceLogRetentionPolicyCfg)
571      {
572        fileRetention.setMinFreeSpaceRequired(((FreeDiskSpaceLogRetentionPolicyCfg) policyConfig).getFreeDiskSpace());
573      }
574      else if (policyConfig instanceof SizeLimitLogRetentionPolicyCfg)
575      {
576        fileRetention.setMaxDiskSpaceToUse(((SizeLimitLogRetentionPolicyCfg) policyConfig).getDiskSpaceUsed());
577      }
578      else
579      {
580        throw new ConfigException(
581            ERR_COMMON_AUDIT_UNSUPPORTED_LOG_RETENTION_POLICY.get(publisher.getDn(), policyConfig.dn()));
582      }
583    }
584    auditConfig.setFileRetention(fileRetention);
585  }
586
587  /**
588   * Convert the set of provided times of day using 24-hour format "HHmm" to a list of
589   * times of day using duration in minutes, e.g "20 minutes".
590   * <p>
591   * Example: "0230" => "150 minutes"
592   */
593  private List<String> convertTimesOfDay(PublisherConfig publisher, FixedTimeLogRotationPolicyCfg policyConfig)
594      throws ConfigException
595  {
596    SortedSet<String> timesOfDay = policyConfig.getTimeOfDay();
597    List<String> times = new ArrayList<>();
598    for (String timeOfDay : timesOfDay)
599    {
600      try
601      {
602        int time = Integer.valueOf(timeOfDay.substring(0, 2)) * 60 + Integer.valueOf(timeOfDay.substring(2, 4));
603        times.add(String.valueOf(time) + " minutes");
604      }
605      catch (NumberFormatException | IndexOutOfBoundsException e)
606      {
607        throw new ConfigException(ERR_COMMON_AUDIT_INVALID_TIME_OF_DAY.get(publisher.getDn(), timeOfDay,
608            StaticUtils.stackTraceToSingleLineString(e)));
609      }
610    }
611    return times;
612  }
613
614  private String getSecurePassword(PublisherConfig publisher, CsvConfigData config)
615  {
616    String fileName = config.getKeystorePinFile();
617    File pinFile = getFileForPath(fileName);
618
619    if (!pinFile.exists())
620    {
621      logger.warn(ERR_COMMON_AUDIT_KEYSTORE_PIN_FILE_MISSING.get(publisher.getDn(), pinFile));
622      return "";
623    }
624
625    try (BufferedReader br = new BufferedReader(new FileReader(pinFile)))
626    {
627      String pinStr = br.readLine();
628      if (pinStr == null)
629      {
630        logger.warn(ERR_COMMON_AUDIT_KEYSTORE_PIN_FILE_CONTAINS_EMPTY_PIN.get(publisher.getDn(), pinFile));
631        return "";
632      }
633      return pinStr;
634    }
635    catch (IOException ioe)
636    {
637      logger.warn(ERR_COMMON_AUDIT_ERROR_READING_KEYSTORE_PIN_FILE.get(publisher.getDn(), pinFile,
638          stackTraceToSingleLineString(ioe)), ioe);
639      return "";
640    }
641  }
642
643  /**
644   * Indicates if the provided log publisher configuration corresponds to a common audit publisher.
645   * <p>
646   * The common audit publisher may not already exist.
647   * <p>
648   * This method must not be used when the corresponding configuration is deleted, because it
649   * implies checking the corresponding configuration entry in the server.
650   *
651   * @param config
652   *          The log publisher configuration.
653   * @return {@code true} if publisher corresponds to a common audit publisher
654   * @throws ConfigException
655   *           If an error occurs
656   */
657  public boolean isCommonAuditConfig(LogPublisherCfg config) throws ConfigException
658  {
659    return new PublisherConfig(config).isCommonAudit();
660  }
661
662  /**
663   * Indicates if the provided log publisher configuration corresponds to a common audit publisher.
664   *
665   * @param config
666   *          The log publisher configuration.
667   * @return {@code true} if publisher is defined for common audit, {@code false} otherwise
668   * @throws ConfigException
669   *           If an error occurs
670   */
671  public boolean isExistingCommonAuditConfig(LogPublisherCfg config) throws ConfigException
672  {
673    String name = getConfigNormalizedName(config);
674    return accessPublishers.containsKey(name) || httpAccessPublishers.containsKey(name);
675  }
676
677  /**
678   * Indicates if HTTP access logging is enabled for common audit.
679   *
680   * @return {@code true} if there is at least one HTTP access logger enabled for common audit.
681   */
682  public boolean isHttpAccessLogEnabled()
683  {
684    return !httpAccessPublishers.isEmpty();
685  }
686
687  private String getConfigNormalizedName(LogPublisherCfg config)
688  {
689    return config.dn().toNormalizedUrlSafeString();
690  }
691
692  /**
693   * Returns the audit service that manages HTTP Access logging.
694   *
695   * @return the request handler that accepts audit events
696   */
697  public RequestHandler getAuditServiceForHttpAccessLog()
698  {
699    return httpAccessAuditService;
700  }
701
702  /**
703   * This class hides all ugly code needed to determine which type of publisher and audit event handler is needed.
704   * <p>
705   * In particular, it allows to retrieve a common configuration that can be used for log publishers that
706   * publish to the same kind of handler.
707   * For example: for CSV handler, DJ configurations for the log publishers contain the same methods but
708   * do not have a common interface (CsvFileAccessLogPublisherCfg vs CsvFileHTTPAccessLogPublisherCfg).
709   */
710  private static class PublisherConfig
711  {
712    private final LogPublisherCfg config;
713    private final boolean isCommonAudit;
714    private LogType logType;
715    private AuditType auditType;
716
717    PublisherConfig(LogPublisherCfg config) throws ConfigException
718    {
719      this.config = config;
720      ConfigEntry configEntry = DirectoryServer.getConfigEntry(config.dn());
721      if (configEntry.hasObjectClass("ds-cfg-csv-file-access-log-publisher"))
722      {
723        auditType = AuditType.CSV;
724        logType = LogType.ACCESS;
725      }
726      else if (configEntry.hasObjectClass("ds-cfg-csv-file-http-access-log-publisher"))
727      {
728        auditType = AuditType.CSV;
729        logType = LogType.HTTP_ACCESS;
730      }
731      else if (configEntry.hasObjectClass("ds-cfg-external-access-log-publisher"))
732      {
733        auditType = AuditType.EXTERNAL;
734        logType = LogType.ACCESS;
735      }
736      else if (configEntry.hasObjectClass("ds-cfg-external-http-access-log-publisher"))
737      {
738        auditType = AuditType.EXTERNAL;
739        logType = LogType.HTTP_ACCESS;
740      }
741      isCommonAudit = auditType != null;
742    }
743
744    DN getDn()
745    {
746      return config.dn();
747    }
748
749    String getName()
750    {
751      return config.dn().getRDN(0).getAttributeValue(0).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}