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 2015 ForgeRock AS.
015*/
016package org.forgerock.openig.filter;
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.net.URI;
023import java.util.concurrent.TimeUnit;
024
025import org.forgerock.audit.events.AccessAuditEventBuilder;
026import org.forgerock.http.Filter;
027import org.forgerock.http.Handler;
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.http.routing.UriRouterContext;
033import org.forgerock.json.resource.CreateRequest;
034import org.forgerock.json.resource.RequestHandler;
035import org.forgerock.services.context.ClientContext;
036import org.forgerock.services.context.Context;
037import org.forgerock.services.context.RequestAuditContext;
038import org.forgerock.util.promise.NeverThrowsException;
039import org.forgerock.util.promise.Promise;
040import org.forgerock.util.promise.ResultHandler;
041import org.forgerock.util.time.TimeService;
042
043/**
044 * This filter aims to send some access audit events to the AuditService managed as a CREST handler.
045 */
046public class HttpAccessAuditFilter implements Filter {
047
048    private final RequestHandler auditServiceHandler;
049    private final TimeService time;
050
051    /**
052     * Constructs a new HttpAccessAuditFilter.
053     *
054     * @param auditServiceHandler The {@link RequestHandler} to publish the events.
055     * @param time The {@link TimeService} to use.
056     */
057    public HttpAccessAuditFilter(RequestHandler auditServiceHandler, TimeService time) {
058        this.auditServiceHandler = auditServiceHandler;
059        this.time = time;
060    }
061
062    @Override
063    public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) {
064        ClientContext clientContext = context.asContext(ClientContext.class);
065
066        AccessAuditEventBuilder<?> accessAuditEventBuilder = accessEvent();
067
068        accessAuditEventBuilder
069                .eventName("OPENIG-HTTP-ACCESS")
070                .timestamp(time.now())
071                .transactionIdFromContext(context)
072                .serverFromContext(clientContext)
073                .clientFromContext(clientContext)
074                .httpRequest(clientContext.isSecure(),
075                             request.getMethod(),
076                             getRequestPath(getURI(context, request)),
077                             new Form().fromRequestQuery(request),
078                             request.getHeaders().copyAsMultiMapOfStrings());
079
080        // We do not expect any RuntimeException as the downstream handler will have to take care
081        // of that case themselves.
082        return next.handle(context, request)
083                .thenOnResult(onResult(context, accessAuditEventBuilder));
084    }
085
086    private static URI getURI(Context context, Request request) {
087        if (context.containsContext(UriRouterContext.class)) {
088            UriRouterContext uriRouterContext = context.asContext(UriRouterContext.class);
089            return uriRouterContext.getOriginalUri();
090        } else {
091            return request.getUri().asURI();
092        }
093    }
094
095    // See HttpContext.getRequestPath
096    private static String getRequestPath(URI uri) {
097        return new StringBuilder()
098            .append(uri.getScheme())
099            .append("://")
100            .append(uri.getRawAuthority())
101            .append(uri.getRawPath()).toString();
102    }
103
104    private ResultHandler<? super Response> onResult(final Context context,
105                                                     final AccessAuditEventBuilder<?> accessAuditEventBuilder) {
106        return new ResultHandler<Response>() {
107            @Override
108            public void handleResult(Response response) {
109                sendAuditEvent(response, context, accessAuditEventBuilder);
110            }
111
112        };
113    }
114
115    private void sendAuditEvent(final Response response,
116                                final Context context,
117                                final AccessAuditEventBuilder<?> accessAuditEventBuilder) {
118        RequestAuditContext requestAuditContext = context.asContext(RequestAuditContext.class);
119
120        if (response != null) {
121            long elapsedTime = time.now() - requestAuditContext.getRequestReceivedTime();
122            accessAuditEventBuilder.httpResponse(response.getHeaders().copyAsMultiMapOfStrings());
123            accessAuditEventBuilder.response(mapResponseStatus(response.getStatus()),
124                                             String.valueOf(response.getStatus().getCode()),
125                                             elapsedTime,
126                                             TimeUnit.MILLISECONDS);
127
128            CreateRequest request = newCreateRequest(resourcePath("/access"),
129                                                     accessAuditEventBuilder.toEvent().getValue());
130            auditServiceHandler.handleCreate(context, request);
131        }
132    }
133
134    private static AccessAuditEventBuilder.ResponseStatus mapResponseStatus(Status status) {
135        switch(status.getFamily()) {
136        case CLIENT_ERROR:
137        case SERVER_ERROR:
138            return AccessAuditEventBuilder.ResponseStatus.FAILED;
139        default:
140            return AccessAuditEventBuilder.ResponseStatus.SUCCESSFUL;
141        }
142    }
143}