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