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 2006-2008 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS.
026 */
027package org.opends.server.loggers;
028
029import static org.forgerock.opendj.ldap.ResultCode.*;
030import static org.opends.messages.ConfigMessages.*;
031import static org.opends.server.util.ServerConstants.*;
032import static org.opends.server.util.StaticUtils.*;
033
034import java.io.File;
035import java.io.IOException;
036import java.util.List;
037
038import org.forgerock.i18n.LocalizableMessage;
039import org.forgerock.opendj.config.server.ConfigChangeResult;
040import org.forgerock.opendj.config.server.ConfigException;
041import org.forgerock.opendj.ldap.ByteSequence;
042import org.forgerock.opendj.ldap.ByteString;
043import org.opends.server.admin.server.ConfigurationChangeListener;
044import org.opends.server.admin.std.server.FileBasedAuditLogPublisherCfg;
045import org.opends.server.core.*;
046import org.opends.server.types.*;
047import org.opends.server.util.Base64;
048import org.opends.server.util.StaticUtils;
049import org.opends.server.util.TimeThread;
050
051/** This class provides the implementation of the audit logger used by the directory server. */
052public final class TextAuditLogPublisher extends
053    AbstractTextAccessLogPublisher<FileBasedAuditLogPublisherCfg> implements
054    ConfigurationChangeListener<FileBasedAuditLogPublisherCfg>
055{
056  private TextWriter writer;
057  private FileBasedAuditLogPublisherCfg cfg;
058
059  @Override
060  public ConfigChangeResult applyConfigurationChange(FileBasedAuditLogPublisherCfg config)
061  {
062    final ConfigChangeResult ccr = new ConfigChangeResult();
063
064    try
065    {
066      // Determine the writer we are using. If we were writing asynchronously,
067      // we need to modify the underlying writer.
068      TextWriter currentWriter;
069      if (writer instanceof AsynchronousTextWriter)
070      {
071        currentWriter = ((AsynchronousTextWriter) writer).getWrappedWriter();
072      }
073      else
074      {
075        currentWriter = writer;
076      }
077
078      if (currentWriter instanceof MultifileTextWriter)
079      {
080        final MultifileTextWriter mfWriter = (MultifileTextWriter) currentWriter;
081        configure(mfWriter, config);
082
083        if (config.isAsynchronous())
084        {
085          if (writer instanceof AsynchronousTextWriter)
086          {
087            if (hasAsyncConfigChanged(config))
088            {
089              // reinstantiate
090              final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
091              writer = newAsyncWriter(mfWriter, config);
092              previousWriter.shutdown(false);
093            }
094          }
095          else
096          {
097            // turn async text writer on
098            writer = newAsyncWriter(mfWriter, config);
099          }
100        }
101        else
102        {
103          if (writer instanceof AsynchronousTextWriter)
104          {
105            // asynchronous is being turned off, remove async text writers.
106            final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
107            writer = mfWriter;
108            previousWriter.shutdown(false);
109          }
110        }
111
112        if (cfg.isAsynchronous() && config.isAsynchronous()
113            && cfg.getQueueSize() != config.getQueueSize())
114        {
115          ccr.setAdminActionRequired(true);
116        }
117
118        cfg = config;
119      }
120    }
121    catch (Exception e)
122    {
123      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
124      ccr.addMessage(ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(
125          config.dn(), stackTraceToSingleLineString(e)));
126    }
127
128    return ccr;
129  }
130
131  private void configure(MultifileTextWriter mfWriter, FileBasedAuditLogPublisherCfg config) throws DirectoryException
132  {
133    final FilePermission perm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
134    final boolean writerAutoFlush = config.isAutoFlush() && !config.isAsynchronous();
135
136    final File logFile = getLogFile(config);
137    final FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
138
139    mfWriter.setNamingPolicy(fnPolicy);
140    mfWriter.setFilePermissions(perm);
141    mfWriter.setAppend(config.isAppend());
142    mfWriter.setAutoFlush(writerAutoFlush);
143    mfWriter.setBufferSize((int) config.getBufferSize());
144    mfWriter.setInterval(config.getTimeInterval());
145
146    mfWriter.removeAllRetentionPolicies();
147    mfWriter.removeAllRotationPolicies();
148    for (final DN dn : config.getRotationPolicyDNs())
149    {
150      mfWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
151    }
152    for (final DN dn : config.getRetentionPolicyDNs())
153    {
154      mfWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
155    }
156  }
157
158  private File getLogFile(final FileBasedAuditLogPublisherCfg config)
159  {
160    return getFileForPath(config.getLogFile());
161  }
162
163  private boolean hasAsyncConfigChanged(FileBasedAuditLogPublisherCfg newConfig)
164  {
165    return !cfg.dn().equals(newConfig.dn())
166        && cfg.isAutoFlush() != newConfig.isAutoFlush()
167        && cfg.getQueueSize() != newConfig.getQueueSize();
168  }
169
170  @Override
171  protected void close0()
172  {
173    writer.shutdown();
174    cfg.removeFileBasedAuditChangeListener(this);
175  }
176
177  @Override
178  public void initializeLogPublisher(FileBasedAuditLogPublisherCfg cfg, ServerContext serverContext)
179      throws ConfigException, InitializationException
180  {
181    File logFile = getLogFile(cfg);
182    FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
183
184    try
185    {
186      final FilePermission perm = FilePermission.decodeUNIXMode(cfg.getLogFilePermissions());
187      final LogPublisherErrorHandler errorHandler = new LogPublisherErrorHandler(cfg.dn());
188      final boolean writerAutoFlush = cfg.isAutoFlush() && !cfg.isAsynchronous();
189
190      MultifileTextWriter writer = new MultifileTextWriter("Multifile Text Writer for " + cfg.dn(),
191          cfg.getTimeInterval(), fnPolicy, perm, errorHandler, "UTF-8",
192          writerAutoFlush, cfg.isAppend(), (int) cfg.getBufferSize());
193
194      // Validate retention and rotation policies.
195      for (DN dn : cfg.getRotationPolicyDNs())
196      {
197        writer.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
198      }
199      for (DN dn : cfg.getRetentionPolicyDNs())
200      {
201        writer.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
202      }
203
204      if (cfg.isAsynchronous())
205      {
206        this.writer = newAsyncWriter(writer, cfg);
207      }
208      else
209      {
210        this.writer = writer;
211      }
212    }
213    catch (DirectoryException e)
214    {
215      throw new InitializationException(
216          ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(cfg.dn(), e), e);
217    }
218    catch (IOException e)
219    {
220      throw new InitializationException(
221          ERR_CONFIG_LOGGING_CANNOT_OPEN_FILE.get(logFile, cfg.dn(), e), e);
222    }
223
224    initializeFilters(cfg);
225    this.cfg = cfg;
226    cfg.addFileBasedAuditChangeListener(this);
227  }
228
229  private AsynchronousTextWriter newAsyncWriter(MultifileTextWriter writer, FileBasedAuditLogPublisherCfg cfg)
230  {
231    String name = "Asynchronous Text Writer for " + cfg.dn();
232    return new AsynchronousTextWriter(name, cfg.getQueueSize(), cfg.isAutoFlush(), writer);
233  }
234
235  @Override
236  public boolean isConfigurationAcceptable(
237      FileBasedAuditLogPublisherCfg configuration,
238      List<LocalizableMessage> unacceptableReasons)
239  {
240    return isFilterConfigurationAcceptable(configuration, unacceptableReasons)
241        && isConfigurationChangeAcceptable(configuration, unacceptableReasons);
242  }
243
244  @Override
245  public boolean isConfigurationChangeAcceptable(
246      FileBasedAuditLogPublisherCfg config, List<LocalizableMessage> unacceptableReasons)
247  {
248    // Make sure the permission is valid.
249    try
250    {
251      FilePermission filePerm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
252      if (!filePerm.isOwnerWritable())
253      {
254        LocalizableMessage message = ERR_CONFIG_LOGGING_INSANE_MODE.get(config.getLogFilePermissions());
255        unacceptableReasons.add(message);
256        return false;
257      }
258    }
259    catch (DirectoryException e)
260    {
261      unacceptableReasons.add(ERR_CONFIG_LOGGING_MODE_INVALID.get(config.getLogFilePermissions(), e));
262      return false;
263    }
264
265    return true;
266  }
267
268  @Override
269  public void logAddResponse(AddOperation addOperation)
270  {
271    if (!isLoggable(addOperation))
272    {
273      return;
274    }
275
276    StringBuilder buffer = new StringBuilder(50);
277    appendHeader(addOperation, buffer);
278
279    buffer.append("dn:");
280    encodeValue(addOperation.getEntryDN().toString(), buffer);
281    buffer.append(EOL);
282
283    buffer.append("changetype: add");
284    buffer.append(EOL);
285
286    for (String ocName : addOperation.getObjectClasses().values())
287    {
288      buffer.append("objectClass: ");
289      buffer.append(ocName);
290      buffer.append(EOL);
291    }
292
293    for (List<Attribute> attrList : addOperation.getUserAttributes().values())
294    {
295      for (Attribute a : attrList)
296      {
297        append(buffer, a);
298      }
299    }
300
301    for (List<Attribute> attrList : addOperation.getOperationalAttributes().values())
302    {
303      for (Attribute a : attrList)
304      {
305        append(buffer, a);
306      }
307    }
308
309    writer.writeRecord(buffer.toString());
310  }
311
312  @Override
313  public void logDeleteResponse(DeleteOperation deleteOperation)
314  {
315    if (!isLoggable(deleteOperation))
316    {
317      return;
318    }
319
320    StringBuilder buffer = new StringBuilder(50);
321    appendHeader(deleteOperation, buffer);
322
323    buffer.append("dn:");
324    encodeValue(deleteOperation.getEntryDN().toString(), buffer);
325    buffer.append(EOL);
326
327    buffer.append("changetype: delete");
328    buffer.append(EOL);
329
330    writer.writeRecord(buffer.toString());
331  }
332
333  @Override
334  public void logModifyDNResponse(ModifyDNOperation modifyDNOperation)
335  {
336    if (!isLoggable(modifyDNOperation))
337    {
338      return;
339    }
340
341    StringBuilder buffer = new StringBuilder(50);
342    appendHeader(modifyDNOperation, buffer);
343
344    buffer.append("dn:");
345    encodeValue(modifyDNOperation.getEntryDN().toString(), buffer);
346    buffer.append(EOL);
347
348    buffer.append("changetype: moddn");
349    buffer.append(EOL);
350
351    buffer.append("newrdn:");
352    encodeValue(modifyDNOperation.getNewRDN().toString(), buffer);
353    buffer.append(EOL);
354
355    buffer.append("deleteoldrdn: ");
356    if (modifyDNOperation.deleteOldRDN())
357    {
358      buffer.append("1");
359    }
360    else
361    {
362      buffer.append("0");
363    }
364    buffer.append(EOL);
365
366    DN newSuperior = modifyDNOperation.getNewSuperior();
367    if (newSuperior != null)
368    {
369      buffer.append("newsuperior:");
370      encodeValue(newSuperior.toString(), buffer);
371      buffer.append(EOL);
372    }
373
374    writer.writeRecord(buffer.toString());
375  }
376
377  @Override
378  public void logModifyResponse(ModifyOperation modifyOperation)
379  {
380    if (!isLoggable(modifyOperation))
381    {
382      return;
383    }
384
385    StringBuilder buffer = new StringBuilder(50);
386    appendHeader(modifyOperation, buffer);
387
388    buffer.append("dn:");
389    encodeValue(modifyOperation.getEntryDN().toString(), buffer);
390    buffer.append(EOL);
391
392    buffer.append("changetype: modify");
393    buffer.append(EOL);
394
395    boolean first = true;
396    for (Modification mod : modifyOperation.getModifications())
397    {
398      if (first)
399      {
400        first = false;
401      }
402      else
403      {
404        buffer.append("-");
405        buffer.append(EOL);
406      }
407
408      switch (mod.getModificationType().asEnum())
409      {
410      case ADD:
411        buffer.append("add: ");
412        break;
413      case DELETE:
414        buffer.append("delete: ");
415        break;
416      case REPLACE:
417        buffer.append("replace: ");
418        break;
419      case INCREMENT:
420        buffer.append("increment: ");
421        break;
422      default:
423        continue;
424      }
425
426      Attribute a = mod.getAttribute();
427      buffer.append(a.getName());
428      buffer.append(EOL);
429
430      append(buffer, a);
431    }
432
433    writer.writeRecord(buffer.toString());
434  }
435
436  private void append(StringBuilder buffer, Attribute a)
437  {
438    for (ByteString v : a)
439    {
440      buffer.append(a.getName());
441      buffer.append(":");
442      encodeValue(v, buffer);
443      buffer.append(EOL);
444    }
445  }
446
447  /** Appends the common log header information to the provided buffer. */
448  private void appendHeader(Operation operation, StringBuilder buffer)
449  {
450    buffer.append("# ");
451    buffer.append(TimeThread.getLocalTime());
452    buffer.append("; conn=");
453    buffer.append(operation.getConnectionID());
454    buffer.append("; op=");
455    buffer.append(operation.getOperationID());
456    buffer.append(EOL);
457  }
458
459  /**
460   * Appends the appropriately-encoded attribute value to the provided
461   * buffer.
462   *
463   * @param str
464   *          The ASN.1 octet string containing the value to append.
465   * @param buffer
466   *          The buffer to which to append the value.
467   */
468  private void encodeValue(ByteSequence str, StringBuilder buffer)
469  {
470    if(StaticUtils.needsBase64Encoding(str))
471    {
472      buffer.append(": ");
473      buffer.append(Base64.encode(str));
474    }
475    else
476    {
477      buffer.append(" ");
478      buffer.append(str.toString());
479    }
480  }
481
482  /**
483   * Appends the appropriately-encoded attribute value to the provided
484   * buffer.
485   *
486   * @param str
487   *          The string containing the value to append.
488   * @param buffer
489   *          The buffer to which to append the value.
490   */
491  private void encodeValue(String str, StringBuilder buffer)
492  {
493    if (StaticUtils.needsBase64Encoding(str))
494    {
495      buffer.append(": ");
496      buffer.append(Base64.encode(getBytes(str)));
497    }
498    else
499    {
500      buffer.append(" ");
501      buffer.append(str);
502    }
503  }
504
505  /** Determines whether the provided operation should be logged. */
506  private boolean isLoggable(Operation operation)
507  {
508    return operation.getResultCode() == SUCCESS
509        && isResponseLoggable(operation);
510  }
511}