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}