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 2013-2015 ForgeRock AS.
015 */
016package org.opends.server.protocols.http;
017
018import static org.forgerock.audit.events.AccessAuditEventBuilder.accessEvent;
019import static org.forgerock.json.resource.Requests.newCreateRequest;
020import static org.forgerock.json.resource.ResourcePath.resourcePath;
021
022import java.util.concurrent.TimeUnit;
023
024import org.forgerock.audit.events.AccessAuditEventBuilder;
025import org.forgerock.http.Filter;
026import org.forgerock.http.Handler;
027import org.forgerock.http.MutableUri;
028import org.forgerock.http.protocol.Form;
029import org.forgerock.http.protocol.Request;
030import org.forgerock.http.protocol.Response;
031import org.forgerock.http.protocol.Status;
032import org.forgerock.json.resource.CreateRequest;
033import org.forgerock.json.resource.RequestHandler;
034import org.forgerock.services.context.ClientContext;
035import org.forgerock.services.context.Context;
036import org.forgerock.services.context.RequestAuditContext;
037import org.forgerock.util.promise.NeverThrowsException;
038import org.forgerock.util.promise.Promise;
039import org.forgerock.util.promise.PromiseImpl;
040import org.forgerock.util.promise.ResultHandler;
041import org.forgerock.util.promise.RuntimeExceptionHandler;
042import org.forgerock.util.time.TimeService;
043
044/**
045 * This filter aims to send some access audit events to the AuditService managed as a CREST handler.
046 */
047public class CommonAuditHttpAccessAuditFilter implements Filter {
048
049    private static Response newInternalServerError() {
050        return new Response(Status.INTERNAL_SERVER_ERROR);
051
052    }
053
054    private final RequestHandler auditServiceHandler;
055    private final TimeService time;
056    private final String productName;
057
058    /**
059     * Constructs a new HttpAccessAuditFilter.
060     *
061     * @param productName The name of product generating the event.
062     * @param auditServiceHandler The {@link RequestHandler} to publish the events.
063     * @param time The {@link TimeService} to use.
064     */
065    public CommonAuditHttpAccessAuditFilter(String productName, RequestHandler auditServiceHandler, TimeService time) {
066        this.productName = productName;
067        this.auditServiceHandler = auditServiceHandler;
068        this.time = time;
069    }
070
071    @Override
072    public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) {
073        ClientContext clientContext = context.asContext(ClientContext.class);
074
075        AccessAuditEventBuilder<?> accessAuditEventBuilder = accessEvent();
076
077        String protocol = clientContext.isSecure() ? "HTTPS" : "HTTP";
078        accessAuditEventBuilder
079                .eventName(productName + "-" + protocol + "-ACCESS")
080                .timestamp(time.now())
081                .transactionIdFromContext(context)
082                .serverFromContext(clientContext)
083                .clientFromContext(clientContext)
084                .httpRequest(clientContext.isSecure(),
085                             request.getMethod(),
086                             getRequestPath(request.getUri()),
087                             new Form().fromRequestQuery(request),
088                             request.getHeaders().copyAsMultiMapOfStrings());
089
090        final PromiseImpl<Response, NeverThrowsException> promiseImpl = PromiseImpl.create();
091        try {
092            next.handle(context, request)
093                    .thenOnResult(onResult(context, accessAuditEventBuilder, promiseImpl))
094                    .thenOnRuntimeException(
095                            onRuntimeException(context, accessAuditEventBuilder, promiseImpl));
096        } catch (RuntimeException e) {
097            onRuntimeException(context, accessAuditEventBuilder, promiseImpl).handleRuntimeException(e);
098        }
099
100        return promiseImpl;
101    }
102
103    // See HttpContext.getRequestPath
104    private String getRequestPath(MutableUri uri) {
105        return new StringBuilder()
106            .append(uri.getScheme())
107            .append("://")
108            .append(uri.getRawAuthority())
109            .append(uri.getRawPath()).toString();
110    }
111
112    private ResultHandler<? super Response> onResult(final Context context,
113                                                     final AccessAuditEventBuilder<?> accessAuditEventBuilder,
114                                                     final PromiseImpl<Response, NeverThrowsException> promiseImpl) {
115        return new ResultHandler<Response>() {
116            @Override
117            public void handleResult(Response response) {
118                sendAuditEvent(response, context, accessAuditEventBuilder);
119                promiseImpl.handleResult(response);
120            }
121
122        };
123    }
124
125    private RuntimeExceptionHandler onRuntimeException(final Context context,
126            final AccessAuditEventBuilder<?> accessAuditEventBuilder,
127            final PromiseImpl<Response, NeverThrowsException> promiseImpl) {
128        return new RuntimeExceptionHandler() {
129            @Override
130            public void handleRuntimeException(RuntimeException exception) {
131                // TODO How to be sure that the final status code sent back with the response will be a 500 ?
132                final Response errorResponse = newInternalServerError();
133                sendAuditEvent(errorResponse, context, accessAuditEventBuilder);
134                promiseImpl.handleResult(errorResponse.setCause(exception));
135            }
136        };
137    }
138
139    private void sendAuditEvent(final Response response,
140                                final Context context,
141                                final AccessAuditEventBuilder<?> accessAuditEventBuilder) {
142        RequestAuditContext requestAuditContext = context.asContext(RequestAuditContext.class);
143        long elapsedTime = time.now() - requestAuditContext.getRequestReceivedTime();
144        accessAuditEventBuilder.httpResponse(response.getHeaders().copyAsMultiMapOfStrings());
145        accessAuditEventBuilder.response(mapResponseStatus(response.getStatus()),
146                                         String.valueOf(response.getStatus().getCode()),
147                                         elapsedTime,
148                                         TimeUnit.MILLISECONDS);
149
150        CreateRequest request =
151            newCreateRequest(resourcePath("/http-access"), accessAuditEventBuilder.toEvent().getValue());
152        auditServiceHandler.handleCreate(context, request);
153    }
154
155    private static AccessAuditEventBuilder.ResponseStatus mapResponseStatus(Status status) {
156        switch(status.getFamily()) {
157        case CLIENT_ERROR:
158        case SERVER_ERROR:
159            return AccessAuditEventBuilder.ResponseStatus.FAILED;
160        default:
161            return AccessAuditEventBuilder.ResponseStatus.SUCCESSFUL;
162        }
163    }
164}