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}