/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2015 ForgeRock AS */ package org.opends.server.loggers; import static org.opends.messages.LoggerMessages.*; import static org.forgerock.json.JsonValue.json; import static org.forgerock.json.resource.Requests.newCreateRequest; import static org.forgerock.json.resource.ResourcePath.resourcePath; import static org.opends.server.loggers.CommonAudit.DEFAULT_TRANSACTION_ID; import static org.opends.server.loggers.OpenDJAccessAuditEventBuilder.openDJAccessEvent; import static org.opends.server.types.AuthenticationType.SASL; import java.util.List; import java.util.concurrent.TimeUnit; import org.forgerock.audit.events.AccessAuditEventBuilder.ResponseStatus; import org.forgerock.audit.events.AuditEvent; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.LocalizableMessageBuilder; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.json.resource.CreateRequest; import org.forgerock.json.resource.RequestHandler; import org.forgerock.json.resource.ResourceException; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.services.context.RootContext; import org.forgerock.util.Pair; import org.forgerock.util.promise.ExceptionHandler; import org.forgerock.util.promise.RuntimeExceptionHandler; import org.opends.server.admin.std.server.AccessLogPublisherCfg; import org.opends.server.api.ClientConnection; import org.opends.server.api.ExtendedOperationHandler; import org.opends.server.controls.TransactionIdControl; import org.opends.server.core.AbandonOperation; import org.opends.server.core.AddOperation; import org.opends.server.core.BindOperation; import org.opends.server.core.CompareOperation; import org.opends.server.core.DeleteOperation; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ExtendedOperation; import org.opends.server.core.ModifyDNOperation; import org.opends.server.core.ModifyOperation; import org.opends.server.core.SearchOperation; import org.opends.server.core.ServerContext; import org.opends.server.core.UnbindOperation; import org.opends.server.types.AuthenticationInfo; import org.opends.server.types.DN; import org.opends.server.types.DirectoryException; import org.opends.server.types.DisconnectReason; import org.opends.server.types.InitializationException; import org.opends.server.types.Operation; import org.opends.server.util.StaticUtils; /** * Publishes access events to Common Audit. * * @param <T> the type of configuration */ abstract class CommonAuditAccessLogPublisher<T extends AccessLogPublisherCfg> extends AbstractTextAccessLogPublisher<T> implements CommonAuditLogPublisher { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** Audit service handler. */ private RequestHandler requestHandler; /** Current configuration for this publisher. */ private T config; private ServerContext serverContext; @Override public void setRequestHandler(RequestHandler handler) { this.requestHandler = handler; } abstract boolean shouldLogControlOids(); T getConfig() { return config; } void setConfig(T config) { this.config = config; } @Override public void initializeLogPublisher(final T cfg, ServerContext serverContext) throws ConfigException, InitializationException { this.serverContext = serverContext; initializeFilters(cfg); config = cfg; } @Override public boolean isConfigurationAcceptable(final T configuration, final List<LocalizableMessage> unacceptableReasons) { return isFilterConfigurationAcceptable(configuration, unacceptableReasons); } @Override public void logAbandonResult(final AbandonOperation abandonOperation) { if (!isResponseLoggable(abandonOperation)) { return; } OpenDJAccessAuditEventBuilder<?> builder = getEventBuilder(abandonOperation, "ABANDON"); appendAbandonRequest(abandonOperation, builder); appendResultCodeAndMessage(abandonOperation, builder); sendEvent(builder.toEvent()); } @Override public void logAddResponse(final AddOperation addOperation) { if (!isResponseLoggable(addOperation)) { return; } OpenDJAccessAuditEventBuilder<?> builder = getEventBuilder(addOperation, "ADD"); appendAddRequest(addOperation, builder); appendResultCodeAndMessage(addOperation, builder); DN proxiedAuthorizationDN = addOperation.getProxiedAuthorizationDN(); appendProxiedAuthorizationDNIfNeeded(builder, proxiedAuthorizationDN); sendEvent(builder.toEvent()); } @Override public void logBindResponse(final BindOperation bindOperation) { if (!isResponseLoggable(bindOperation)) { return; } OpenDJAccessAuditEventBuilder<?> builder = getEventBuilder(bindOperation, "BIND"); appendBindRequest(bindOperation, builder); appendResultCodeAndMessage(bindOperation, builder); final LocalizableMessage failureMessage = bindOperation.getAuthFailureReason(); if (failureMessage != null) { // this code path is mutually exclusive with the if result code is success // down below builder.ldapFailureMessage(failureMessage.toString()); if (bindOperation.getSASLMechanism() != null && bindOperation.getSASLAuthUserEntry() != null) { // SASL bind and we have successfully found a user entry for auth builder.userId(bindOperation.getSASLAuthUserEntry().getName().toString()); } else { // SASL bind failed to find user entry for auth or simple bind builder.userId(bindOperation.getRawBindDN().toString()); } } if (bindOperation.getResultCode() == ResultCode.SUCCESS) { // this code path is mutually exclusive with the if failure message exist // just above final AuthenticationInfo authInfo = bindOperation.getAuthenticationInfo(); if (authInfo != null) { final DN authDN = authInfo.getAuthenticationDN(); if (authDN != null) { builder.userId(authDN.toString()); final DN authzDN = authInfo.getAuthorizationDN(); if (!authDN.equals(authzDN)) { builder.runAs(authzDN.toString()); } } else { builder.userId(""); } } } sendEvent(builder.toEvent()); } @Override public void logCompareResponse(final CompareOperation compareOperation) { if (!isResponseLoggable(compareOperation)) { return; } OpenDJAccessAuditEventBuilder<?> builder = getEventBuilder(compareOperation, "COMPARE"); appendCompareRequest(compareOperation, builder); appendResultCodeAndMessage(compareOperation, builder); DN proxiedAuthorizationDN = compareOperation.getProxiedAuthorizationDN(); appendProxiedAuthorizationDNIfNeeded(builder, proxiedAuthorizationDN); sendEvent(builder.toEvent()); } private void appendProxiedAuthorizationDNIfNeeded(OpenDJAccessAuditEventBuilder<?> builder, DN proxiedAuthorizationDN) { if (proxiedAuthorizationDN != null) { builder.runAs(proxiedAuthorizationDN.toString()); } } @Override public void logConnect(final ClientConnection clientConnection) { if (!isConnectLoggable(clientConnection)) { return; } OpenDJAccessAuditEventBuilder<?> builder = openDJAccessEvent() .eventName("DJ-" + clientConnection.getProtocol() + "-" + "CONNECT") .client(clientConnection.getClientAddress(), clientConnection.getClientPort()) .server(clientConnection.getServerAddress(), clientConnection.getServerPort()) .request(clientConnection.getProtocol(), "CONNECT") .transactionId(CommonAudit.DEFAULT_TRANSACTION_ID) .response(ResponseStatus.SUCCESSFUL, String.valueOf(ResultCode.SUCCESS.intValue()), 0, TimeUnit.MILLISECONDS) .ldapConnectionId(clientConnection.getConnectionID()); sendEvent(builder.toEvent()); } @Override public void logDeleteResponse(final DeleteOperation deleteOperation) { if (!isResponseLoggable(deleteOperation)) { return; } OpenDJAccessAuditEventBuilder<?> builder = getEventBuilder(deleteOperation, "DELETE"); appendDeleteRequest(deleteOperation, builder); appendResultCodeAndMessage(deleteOperation, builder); DN proxiedAuthorizationDN = deleteOperation.getProxiedAuthorizationDN(); appendProxiedAuthorizationDNIfNeeded(builder, proxiedAuthorizationDN); sendEvent(builder.toEvent()); } @Override public void logDisconnect(final ClientConnection clientConnection, final DisconnectReason disconnectReason, final LocalizableMessage message) { if (!isDisconnectLoggable(clientConnection)) { return; } OpenDJAccessAuditEventBuilder<?> builder = openDJAccessEvent() .eventName("DJ-" + clientConnection.getProtocol() + "-" + "DISCONNECT") .client(clientConnection.getClientAddress(), clientConnection.getClientPort()) .server(clientConnection.getServerAddress(), clientConnection.getServerPort()) .request(clientConnection.getProtocol(), "DISCONNECT") .transactionId(CommonAudit.DEFAULT_TRANSACTION_ID) .response(ResponseStatus.SUCCESSFUL, String.valueOf(ResultCode.SUCCESS.intValue()), 0, TimeUnit.MILLISECONDS) .ldapConnectionId(clientConnection.getConnectionID()) .ldapReason(disconnectReason) .ldapMessage(message); sendEvent(builder.toEvent()); } @Override public void logExtendedResponse(final ExtendedOperation extendedOperation) { if (!isResponseLoggable(extendedOperation)) { return; } OpenDJAccessAuditEventBuilder<?> builder = getEventBuilder(extendedOperation, "EXTENDED"); appendExtendedRequest(extendedOperation, builder); appendResultCodeAndMessage(extendedOperation, builder); final String oid = extendedOperation.getResponseOID(); if (oid != null) { final ExtendedOperationHandler<?> extOpHandler = DirectoryServer.getExtendedOperationHandler(oid); if (extOpHandler != null) { String name = extOpHandler.getExtendedOperationName(); builder.ldapName(name); } builder.ldapOid(oid); } sendEvent(builder.toEvent()); } @Override public void logModifyDNResponse(final ModifyDNOperation modifyDNOperation) { if (!isResponseLoggable(modifyDNOperation)) { return; } OpenDJAccessAuditEventBuilder<?> builder = getEventBuilder(modifyDNOperation, "MODIFYDN"); appendModifyDNRequest(modifyDNOperation, builder); appendResultCodeAndMessage(modifyDNOperation, builder); DN proxiedAuthorizationDN = modifyDNOperation.getProxiedAuthorizationDN(); appendProxiedAuthorizationDNIfNeeded(builder, proxiedAuthorizationDN); sendEvent(builder.toEvent()); } @Override public void logModifyResponse(final ModifyOperation modifyOperation) { if (!isResponseLoggable(modifyOperation)) { return; } OpenDJAccessAuditEventBuilder<?> builder = getEventBuilder(modifyOperation, "MODIFY"); appendModifyRequest(modifyOperation, builder); appendResultCodeAndMessage(modifyOperation, builder); DN proxiedAuthorizationDN = modifyOperation.getProxiedAuthorizationDN(); appendProxiedAuthorizationDNIfNeeded(builder, proxiedAuthorizationDN); sendEvent(builder.toEvent()); } @Override public void logSearchResultDone(final SearchOperation searchOperation) { if (!isResponseLoggable(searchOperation)) { return; } OpenDJAccessAuditEventBuilder<?> builder = getEventBuilder(searchOperation, "SEARCH"); builder .ldapSearch(searchOperation) .ldapNEntries(searchOperation.getEntriesSent()); appendResultCodeAndMessage(searchOperation, builder); DN proxiedAuthorizationDN = searchOperation.getProxiedAuthorizationDN(); appendProxiedAuthorizationDNIfNeeded(builder, proxiedAuthorizationDN); sendEvent(builder.toEvent()); } @Override public void logUnbind(final UnbindOperation unbindOperation) { if (!isRequestLoggable(unbindOperation)) { return; } sendEvent(getEventBuilder(unbindOperation, "UNBIND").toEvent()); } @Override protected void close0() { // nothing to do because closing is managed in the CommonAudit class } private void appendAbandonRequest(final AbandonOperation abandonOperation, final OpenDJAccessAuditEventBuilder<?> builder) { builder.ldapIdToAbandon(abandonOperation.getIDToAbandon()); } private void appendAddRequest(final AddOperation addOperation, OpenDJAccessAuditEventBuilder<?> builder) { builder.ldapDn(addOperation.getRawEntryDN().toString()); } private void appendBindRequest(final BindOperation bindOperation, final OpenDJAccessAuditEventBuilder<?> builder) { builder.ldapProtocolVersion(bindOperation.getProtocolVersion()); final String authType = bindOperation.getAuthenticationType() != SASL ? bindOperation.getAuthenticationType().toString() : "SASL mechanism=" + bindOperation.getSASLMechanism(); builder.ldapAuthType(authType); builder.ldapDn(bindOperation.getRawBindDN().toString()); } private void appendCompareRequest(final CompareOperation compareOperation, final OpenDJAccessAuditEventBuilder<?> builder) { builder.ldapDn(compareOperation.getRawEntryDN().toString()); builder.ldapAttr(compareOperation.getAttributeType().getNameOrOID()); } private void appendDeleteRequest(final DeleteOperation deleteOperation, final OpenDJAccessAuditEventBuilder<?> builder) { builder.ldapDn(deleteOperation.getRawEntryDN().toString()); } private void appendExtendedRequest(final ExtendedOperation extendedOperation, final OpenDJAccessAuditEventBuilder<?> builder) { final String oid = extendedOperation.getRequestOID(); final ExtendedOperationHandler<?> extOpHandler = DirectoryServer.getExtendedOperationHandler(oid); if (extOpHandler != null) { final String name = extOpHandler.getExtendedOperationName(); builder.ldapName(name); } builder.ldapOid(oid); } private void appendModifyDNRequest(final ModifyDNOperation modifyDNOperation, final OpenDJAccessAuditEventBuilder<?> builder) { builder.ldapDn(modifyDNOperation.getRawEntryDN().toString()); builder.ldapModifyDN(modifyDNOperation); } private void appendModifyRequest(final ModifyOperation modifyOperation, final OpenDJAccessAuditEventBuilder<?> builder) { builder.ldapDn(modifyOperation.getRawEntryDN().toString()); } private OpenDJAccessAuditEventBuilder<?> appendResultCodeAndMessage( Operation operation, OpenDJAccessAuditEventBuilder<?> builder) { final LocalizableMessageBuilder message = operation.getErrorMessage(); int resultCode = operation.getResultCode().intValue(); ResponseStatus status = resultCode == 0 ? ResponseStatus.SUCCESSFUL : ResponseStatus.FAILED; Pair<Long, TimeUnit> executionTime = getExecutionTime(operation); if (message != null && message.length() > 0) { builder.responseWithDetail(status, String.valueOf(resultCode), executionTime.getFirst(), executionTime.getSecond(), json(message.toString())); } else { builder.response(status, String.valueOf(resultCode), executionTime.getFirst(), executionTime.getSecond()); } builder.ldapMaskedResultAndMessage(operation); return builder; } /** Returns an event builder with all common fields filled. */ private OpenDJAccessAuditEventBuilder<?> getEventBuilder(final Operation operation, final String opType) { ClientConnection clientConn = operation.getClientConnection(); OpenDJAccessAuditEventBuilder<?> builder = openDJAccessEvent() .eventName("DJ-" + clientConn.getProtocol() + "-" + opType) .client(clientConn.getClientAddress(), clientConn.getClientPort()) .server(clientConn.getServerAddress(), clientConn.getServerPort()) .request(clientConn.getProtocol(), opType) .ldapAdditionalItems(operation) .ldapSync(operation) .ldapIds(operation) .transactionId(getTransactionId(operation)); if (shouldLogControlOids()) { builder.ldapControls(operation); } return builder; } private String getTransactionId(Operation operation) { String transactionId = getTransactionIdFromControl(operation); if (transactionId == null || !serverContext.getCommonAudit().shouldTrustTransactionIds()) { // use a default value transactionId = DEFAULT_TRANSACTION_ID; } return transactionId; } private String getTransactionIdFromControl(Operation operation) { try { TransactionIdControl control = operation.getRequestControl(TransactionIdControl.DECODER); return control != null ? control.getTransactionId() : null; } catch (DirectoryException e) { logger.error(ERR_COMMON_AUDIT_INVALID_TRANSACTION_ID.get(StaticUtils.stackTraceToSingleLineString(e))); } return null; } private Pair<Long,TimeUnit> getExecutionTime(final Operation operation) { Long etime = operation.getProcessingNanoTime(); // if not configured for nanos, use millis return etime <= -1 ? Pair.of(operation.getProcessingTime(), TimeUnit.MILLISECONDS) : Pair.of(etime, TimeUnit.NANOSECONDS); } /** Sends an JSON-encoded event to the audit service. */ private void sendEvent(AuditEvent event) { CreateRequest request = newCreateRequest(resourcePath("/ldap-access"), event.getValue()); requestHandler .handleCreate(new RootContext(), request) .thenOnException(new ExceptionHandler<ResourceException>() { @Override public void handleException(ResourceException e) { logger.error(ERR_COMMON_AUDIT_UNABLE_TO_PROCESS_LOG_EVENT.get(StaticUtils.stackTraceToSingleLineString(e))); } }) .thenOnRuntimeException(new RuntimeExceptionHandler() { @Override public void handleRuntimeException(RuntimeException e) { logger.error(ERR_COMMON_AUDIT_UNABLE_TO_PROCESS_LOG_EVENT.get(StaticUtils.stackTraceToSingleLineString(e))); } }); } }