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}