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