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