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}