/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2013-2015 ForgeRock AS. All Rights Reserved * * The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * http://forgerock.org/license/CDDLv1.0.html * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at http://forgerock.org/license/CDDLv1.0.html * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" */ package org.forgerock.openidm.audit.filter; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.forgerock.audit.events.AccessAuditEventBuilder.ResponseStatus.FAILED; import static org.forgerock.audit.events.AccessAuditEventBuilder.ResponseStatus.SUCCESSFUL; import static org.forgerock.services.context.ClientContext.newInternalClientContext; import java.util.List; import org.forgerock.audit.events.AccessAuditEventBuilder; import org.forgerock.json.resource.ActionRequest; import org.forgerock.json.resource.ActionResponse; import org.forgerock.json.resource.ConnectionFactory; import org.forgerock.json.resource.CreateRequest; import org.forgerock.json.resource.DeleteRequest; import org.forgerock.json.resource.Filter; import org.forgerock.json.resource.PatchRequest; import org.forgerock.json.resource.QueryRequest; import org.forgerock.json.resource.QueryResourceHandler; import org.forgerock.json.resource.QueryResponse; import org.forgerock.json.resource.ReadRequest; import org.forgerock.json.resource.Request; import org.forgerock.json.resource.RequestHandler; import org.forgerock.json.resource.Requests; import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.ResourceResponse; import org.forgerock.json.resource.Response; import org.forgerock.json.resource.UpdateRequest; import org.forgerock.openidm.util.ContextUtil; import org.forgerock.services.context.Context; import org.forgerock.services.context.SecurityContext; import org.forgerock.util.Reject; import org.forgerock.util.promise.ExceptionHandler; import org.forgerock.util.promise.Promise; import org.forgerock.util.promise.ResultHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class creates a {@link Filter} that can be placed on the router to log access events. */ public class AuditFilter implements Filter { private static final Logger LOGGER = LoggerFactory.getLogger(AuditFilter.class); private ConnectionFactory connectionFactory; private AuditFilter() { //prevent instantiation } public AuditFilter(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } public class AuditState { private final long actionTime = System.currentTimeMillis(); private Request request; public AuditState(final Request request) { this.request = request; } } /** * {@inheritDoc} */ @Override public Promise<ActionResponse, ResourceException> filterAction(Context context, ActionRequest request, RequestHandler next) { AuditState state = new AuditState(request); Promise<ActionResponse, ResourceException> promise = next.handleAction(context, request); logAuditAccessEntry(context, state, promise); return promise; } /** * {@inheritDoc} */ @Override public Promise<ResourceResponse, ResourceException> filterCreate(Context context, CreateRequest request, RequestHandler next) { AuditState state = new AuditState(request); Promise<ResourceResponse, ResourceException> promise = next.handleCreate(context, request); logAuditAccessEntry(context, state, promise); return promise; } /** * {@inheritDoc} */ @Override public Promise<ResourceResponse, ResourceException> filterDelete(Context context, DeleteRequest request, RequestHandler next) { AuditState state = new AuditState(request); Promise<ResourceResponse, ResourceException> promise = next.handleDelete(context, request); logAuditAccessEntry(context, state, promise); return promise; } /** * {@inheritDoc} */ @Override public Promise<ResourceResponse, ResourceException> filterPatch(Context context, PatchRequest request, RequestHandler next) { AuditState state = new AuditState(request); Promise<ResourceResponse, ResourceException> promise = next.handlePatch(context, request); logAuditAccessEntry(context, state, promise); return promise; } /** * {@inheritDoc} */ @Override public Promise<QueryResponse, ResourceException> filterQuery(Context context, QueryRequest request, QueryResourceHandler handler, RequestHandler next) { AuditState state = new AuditState(request); Promise<QueryResponse, ResourceException> promise = next.handleQuery(context, request, handler); logAuditAccessEntry(context, state, promise); return promise; } /** * {@inheritDoc} */ @Override public Promise<ResourceResponse, ResourceException> filterRead(Context context, ReadRequest request, RequestHandler next) { AuditState state = new AuditState(request); Promise<ResourceResponse, ResourceException> promise = next.handleRead(context, request); logAuditAccessEntry(context, state, promise); return promise; } /** * {@inheritDoc} */ @Override public Promise<ResourceResponse, ResourceException> filterUpdate(Context context, UpdateRequest request, RequestHandler next) { AuditState state = new AuditState(request); Promise<ResourceResponse, ResourceException> promise = next.handleUpdate(context, request); logAuditAccessEntry(context, state, promise); return promise; } private void logAuditAccessEntry(final Context context, final AuditState state, final Promise<? extends Response, ResourceException> promise) { if (!ContextUtil.isExternal(context)) { // don't log internal requests return; } final OpenIDMAccessAuditEventBuilder accessAuditEventBuilder = new OpenIDMAccessAuditEventBuilder(); accessAuditEventBuilder .rolesFromCrestContext(context) .forHttpRequest(context, state.request) // TODO CAUD-114 .serverFromHttpContext(context) .requestFromCrestRequest(state.request) .clientFromContext(context) .httpFromContext(context) .transactionIdFromContext(context) .eventName("access") .userId(getUserId(context)); promise.thenOnResultOrException( new ResultHandler<Response>() { @Override public void handleResult(Response result) { long now = System.currentTimeMillis(); final long elapsedTime = now - state.actionTime; accessAuditEventBuilder.response(SUCCESSFUL, null, elapsedTime, MILLISECONDS).timestamp(now); } }, new ExceptionHandler<ResourceException>() { @Override public void handleException(ResourceException resourceException) { long now = System.currentTimeMillis(); final long elapsedTime = now - state.actionTime; accessAuditEventBuilder.responseWithDetail(FAILED, String.valueOf(resourceException.getCode()), elapsedTime, MILLISECONDS, resourceException.toJsonValue()).timestamp(now); } }) .thenAlways(new Runnable() { @Override public void run() { try { //log the log entry final CreateRequest createRequest = Requests.newCreateRequest("audit/access", accessAuditEventBuilder.toEvent().getValue()); //wrap the context in a new internal context since we are using the external connection // factory connectionFactory.getConnection().create(newInternalClientContext(context), createRequest); } catch (ResourceException e) { LOGGER.error("Failed to log audit access entry", e); } } }); } public void setConnectionFactory(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } private String getUserId(Context context) { if (context.containsContext(SecurityContext.class)) { return context.asContext(SecurityContext.class).getAuthenticationId(); } else { return ""; } } /** * Extended Commons Audit Event Builder that handles the extended attributes of OpenIdm. */ @SuppressWarnings("unchecked") private class OpenIDMAccessAuditEventBuilder<T extends OpenIDMAccessAuditEventBuilder<T>> extends AccessAuditEventBuilder<T> { public static final String ROLES = "roles"; public T rolesFromCrestContext(final Context context) { if (context.containsContext(SecurityContext.class)) { return roles((List<String>) context.asContext(SecurityContext.class).getAuthorization().get("roles")); } return self(); } public T roles(List<String> roles) { Reject.ifNull(roles); jsonValue.put(ROLES, roles); return self(); } } }