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.audit.events; 017 018import static org.forgerock.json.JsonValue.field; 019import static org.forgerock.json.JsonValue.json; 020import static org.forgerock.json.JsonValue.object; 021 022import java.util.ArrayList; 023import java.util.LinkedHashMap; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.Map; 027import java.util.TreeMap; 028import java.util.concurrent.TimeUnit; 029 030import org.forgerock.http.header.CookieHeader; 031import org.forgerock.http.protocol.Cookie; 032import org.forgerock.json.JsonValue; 033import org.forgerock.json.resource.ActionRequest; 034import org.forgerock.json.resource.Request; 035import org.forgerock.services.context.ClientContext; 036import org.forgerock.services.context.Context; 037import org.forgerock.util.Reject; 038import org.forgerock.util.annotations.VisibleForTesting; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042/** 043 * Builder for audit access events. 044 * <p> 045 * This builder should not be used directly but be specialized for each product to allow to define 046 * new specific fields, e.g 047 * <pre> 048 * <code> 049 * class OpenProductAccessAuditEventBuilder{@code <T extends OpenProductAccessAuditEventBuilder<T>>} 050 extends AccessAuditEventBuilder{@code <T>} { 051 * 052 * protected OpenProductAccessAuditEventBuilder(DnsUtils dnsUtils) { 053 * super(dnsUtils); 054 * } 055 * 056 * public static {@code <T>} OpenProductAccessAuditEventBuilder{@code <?>} productAccessEvent() { 057 * return new OpenProductAccessAuditEventBuilder(new DnsUtils()); 058 * } 059 * 060 * public T someField(String v) { 061 * jsonValue.put("someField", v); 062 * return self(); 063 * } 064 * 065 * ... 066 * } 067 * </code> 068 * </pre> 069 * 070 * @param <T> the type of the builder 071 */ 072public class AccessAuditEventBuilder<T extends AccessAuditEventBuilder<T>> extends AuditEventBuilder<T> { 073 074 public static final String SERVER = "server"; 075 public static final String CLIENT = "client"; 076 public static final String IP = "ip"; 077 public static final String PORT = "port"; 078 public static final String REQUEST = "request"; 079 public static final String PROTOCOL = "protocol"; 080 public static final String OPERATION = "operation"; 081 public static final String SECURE = "secure"; 082 public static final String METHOD = "method"; 083 public static final String DETAIL = "detail"; 084 public static final String PATH = "path"; 085 public static final String QUERY_PARAMETERS = "queryParameters"; 086 public static final String HEADERS = "headers"; 087 public static final String HTTP = "http"; 088 public static final String STATUS = "status"; 089 public static final String STATUS_CODE = "statusCode"; 090 public static final String ELAPSED_TIME = "elapsedTime"; 091 public static final String ELAPSED_TIME_UNITS = "elapsedTimeUnits"; 092 public static final String RESPONSE = "response"; 093 public static final String COOKIES = "cookies"; 094 095 public static final String CREST_PROTOCOL = "CREST"; 096 097 private static final String HTTP_CONTEXT_NAME = "http"; 098 private static final String CLIENT_CONTEXT_NAME = "client"; 099 100 private static final Logger logger = LoggerFactory.getLogger(AccessAuditEventBuilder.class); 101 102 private boolean performReverseDnsLookup = false; 103 104 /** 105 * Starts to build an audit access event. 106 * <p> 107 * Note: it is preferable to use a specialized builder that allow to add 108 * fields specific to a product. 109 * 110 * @return an audit access event builder 111 */ 112 @SuppressWarnings("rawtypes") 113 public static AccessAuditEventBuilder<?> accessEvent() { 114 return new AccessAuditEventBuilder(); 115 } 116 117 /** 118 * Instructs the builder to lookup client.host from client.ip when populating client details. 119 * 120 * @return this builder 121 */ 122 public final T withReverseDnsLookup() { 123 performReverseDnsLookup = true; 124 return self(); 125 } 126 127 /** 128 * @return True if client.host should be looked up from client.ip. 129 */ 130 protected boolean isReverseDnsLookupEnabled() { 131 return performReverseDnsLookup; 132 } 133 134 /** 135 * Sets the provided server values for the event. 136 * 137 * @param ip the ip of the server. 138 * @param port the port of the server. 139 * @return this builder 140 */ 141 public final T server(String ip, int port) { 142 final Object server = object( 143 field(IP, ip), 144 field(PORT, port)); 145 jsonValue.put(SERVER, server); 146 return self(); 147 } 148 149 /** 150 * Sets the provided client ip and port for the event. 151 * 152 * @param ip the ip of the client. 153 * @param port the port of the client. 154 * @return this builder 155 */ 156 public final T client(String ip, int port) { 157 final Object client = object( 158 field(IP, ip), 159 field(PORT, port)); 160 jsonValue.put(CLIENT, client); 161 return self(); 162 } 163 164 /** 165 * Sets the provided client ip for the event. 166 * 167 * @param ip the ip of the client. 168 * @return this builder 169 */ 170 public final T client(String ip) { 171 final Object client = object( 172 field(IP, ip)); 173 jsonValue.put(CLIENT, client); 174 return self(); 175 } 176 177 /** 178 * Sets the provided request details for the event. 179 * 180 * @param protocol the type of request. 181 * @param operation the type of operation (e.g. CREATE, READ, UPDATE, DELETE, PATCH, ACTION, or QUERY). 182 * @return this builder 183 */ 184 public final T request(String protocol, String operation) { 185 final Object request = object( 186 field(PROTOCOL, protocol), 187 field(OPERATION, operation)); 188 jsonValue.put(REQUEST, request); 189 return self(); 190 } 191 192 /** 193 * Sets the provided request details for the event. 194 * 195 * @param protocol the type of request. 196 * @param operation the type of operation (e.g. CREATE, READ, UPDATE, DELETE, PATCH, ACTION, or QUERY). 197 * @param detail additional details relating to the request (e.g. the ACTION name or summary of the payload). 198 * @return this builder 199 */ 200 public final T request(String protocol, String operation, JsonValue detail) { 201 Reject.ifNull(detail); 202 final Object request = object( 203 field(PROTOCOL, protocol), 204 field(OPERATION, operation), 205 field(DETAIL, detail.getObject())); 206 jsonValue.put(REQUEST, request); 207 return self(); 208 } 209 210 /** 211 * Sets the provided HTTP request fields for the event. 212 * 213 * @param secure Was the request secure ? 214 * @param method the HTTP method. 215 * @param path the path of HTTP request. 216 * @param queryParameters the query parameters of HTTP request. 217 * @param headers the list of headers of HTTP request. The headers are optional. 218 * @return this builder 219 */ 220 public final T httpRequest(boolean secure, String method, String path, Map<String, List<String>> queryParameters, 221 Map<String, List<String>> headers) { 222 List<String> cookiesHeader = headers.remove("Cookie"); 223 if (cookiesHeader == null || cookiesHeader.isEmpty()) { 224 cookiesHeader = headers.remove("cookie"); 225 } 226 final List<Cookie> listOfCookies = new LinkedList<>(); 227 if (cookiesHeader != null && !cookiesHeader.isEmpty()) { 228 listOfCookies.addAll(CookieHeader.valueOf(cookiesHeader.get(0)).getCookies()); 229 } 230 231 final Map<String, String> cookies = new LinkedHashMap<>(); 232 for (final Cookie cookie : listOfCookies) { 233 cookies.put(cookie.getName(), cookie.getValue()); 234 } 235 httpRequest(secure, method, path, queryParameters, headers, cookies); 236 return self(); 237 } 238 239 /** 240 * Sets the provided HTTP request fields for the event. 241 * 242 * @param secure Was the request secure ? 243 * @param method the HTTP method. 244 * @param path the path of HTTP request. 245 * @param queryParameters the query parameters of HTTP request. 246 * @param headers the list of headers of HTTP request. The headers are optional. 247 * @param cookies the list of cookies of HTTP request. The cookies are optional. 248 * @return this builder 249 */ 250 public final T httpRequest(boolean secure, String method, String path, Map<String, List<String>> queryParameters, 251 Map<String, List<String>> headers, Map<String, String> cookies) { 252 final Object httpRequest = object( 253 field(SECURE, secure), 254 field(METHOD, method), 255 field(PATH, path), 256 field(QUERY_PARAMETERS, queryParameters), 257 field(HEADERS, headers), 258 field(COOKIES, cookies)); 259 getOrCreateHttp().put(REQUEST, httpRequest); 260 return self(); 261 } 262 263 /** 264 * Sets the provided HTTP fields for the event. 265 * 266 * @param headers the list of headers of HTTP response. The headers are optional. 267 * @return this builder 268 */ 269 public final T httpResponse(Map<String, List<String>> headers) { 270 final Object httpResponse = object(field(HEADERS, headers)); 271 getOrCreateHttp().put(RESPONSE, httpResponse); 272 return self(); 273 } 274 275 @VisibleForTesting 276 JsonValue getOrCreateHttp() { 277 if (jsonValue.get(HTTP).isNull()) { 278 jsonValue.put(HTTP, object()); 279 } 280 return jsonValue.get(HTTP); 281 } 282 283 @VisibleForTesting 284 JsonValue getOrCreateHttpResponse() { 285 if (getOrCreateHttp().get(RESPONSE).isNull()) { 286 getOrCreateHttp().put(RESPONSE, object()); 287 } 288 return getOrCreateHttp().get(RESPONSE); 289 } 290 291 @VisibleForTesting 292 JsonValue getOrCreateHttpResponseCookies() { 293 final JsonValue httpResponse = getOrCreateHttpResponse(); 294 JsonValue cookies = httpResponse.get(COOKIES); 295 if (cookies.isNull()) { 296 httpResponse.put(COOKIES, new ArrayList()); 297 } 298 cookies = httpResponse.get(COOKIES); 299 return cookies; 300 } 301 302 /** 303 * Sets the provided response for the event. 304 * 305 * @param status the status of the operation. 306 * @param statusCode the status code of the operation. 307 * @param elapsedTime the execution time of the action. 308 * @param elapsedTimeUnits the unit of measure for the execution time value. 309 * @return this builder 310 */ 311 public final T response(ResponseStatus status, String statusCode, long elapsedTime, TimeUnit elapsedTimeUnits) { 312 final Object response = object( 313 field(STATUS, status == null ? null : status.toString()), 314 field(STATUS_CODE, statusCode), 315 field(ELAPSED_TIME, elapsedTime), 316 field(ELAPSED_TIME_UNITS, elapsedTimeUnits == null ? null : elapsedTimeUnits.name())); 317 jsonValue.put(RESPONSE, response); 318 return self(); 319 } 320 321 /** 322 * Sets the provided response for the event, with an additional detail. 323 * 324 * @param status the status of the operation. 325 * @param statusCode the status code of the operation. 326 * @param elapsedTime the execution time of the action. 327 * @param elapsedTimeUnits the unit of measure for the execution time value. 328 * @param detail additional details relating to the response (e.g. failure description or summary of the payload). 329 * @return this builder 330 */ 331 public final T responseWithDetail(ResponseStatus status, String statusCode, 332 long elapsedTime, TimeUnit elapsedTimeUnits, JsonValue detail) { 333 Reject.ifNull(detail); 334 final Object response = object( 335 field(STATUS, status == null ? null : status.toString()), 336 field(STATUS_CODE, statusCode), 337 field(ELAPSED_TIME, elapsedTime), 338 field(ELAPSED_TIME_UNITS, elapsedTimeUnits == null ? null : elapsedTimeUnits.name()), 339 field(DETAIL, detail.getObject())); 340 jsonValue.put(RESPONSE, response); 341 return self(); 342 } 343 344 /** 345 * Sets client ip, port and host from <code>ClientContext</code>, if the provided 346 * <code>Context</code> contains a <code>ClientContext</code>. 347 * 348 * @param context The root CHF context. 349 * @return this builder 350 */ 351 public final T clientFromContext(Context context) { 352 if (context.containsContext(ClientContext.class)) { 353 ClientContext clientContext = context.asContext(ClientContext.class); 354 client(clientContext.getRemoteAddress(), clientContext.getRemotePort()); 355 } 356 return self(); 357 } 358 359 /** 360 * Sets the server fields for the event, if the provided 361 * <code>Context</code> contains a <code>ClientContext</code>.. 362 * 363 * @param context the CREST context 364 * @return this builder 365 */ 366 public final T serverFromContext(Context context) { 367 if (context.containsContext(ClientContext.class)) { 368 ClientContext clientContext = context.asContext(ClientContext.class); 369 server(clientContext.getLocalAddress(), clientContext.getLocalPort()); 370 } 371 return self(); 372 } 373 374 /** 375 * Sets HTTP method, path, queryString and headers from <code>HttpContext</code>, if the provided 376 * <code>Context</code> contains a <code>HttpContext</code>. 377 * 378 * @param context The CREST context. 379 * @return this builder 380 */ 381 public final T httpFromContext(Context context) { 382 if (context.containsContext(HTTP_CONTEXT_NAME)) { 383 final JsonValue httpContext = context.getContext(HTTP_CONTEXT_NAME).toJsonValue(); 384 final JsonValue clientContext = context.getContext(CLIENT_CONTEXT_NAME).toJsonValue(); 385 httpRequest(clientContext.get("isSecure").asBoolean(), 386 httpContext.get("method").asString(), 387 httpContext.get("path").asString(), 388 asModifiableCaseSensitiveMap(httpContext.get("parameters").asMapOfList(String.class)), 389 asModifiableCaseInsensitiveMap(httpContext.get("headers").asMapOfList(String.class))); 390 } 391 return self(); 392 } 393 394 private <E> Map<String, List<E>> asModifiableCaseSensitiveMap(Map<String, List<E>> map) { 395 return new LinkedHashMap<>(map); 396 } 397 398 private <E> Map<String, List<E>> asModifiableCaseInsensitiveMap(Map<String, List<E>> map) { 399 TreeMap<String, List<E>> caseInsensitiveMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 400 caseInsensitiveMap.putAll(map); 401 return caseInsensitiveMap; 402 } 403 404 /** 405 * Sets request detail from {@link Request}. 406 * 407 * @param request The CREST request. 408 * @return this builder 409 */ 410 public final T requestFromCrestRequest(Request request) { 411 final String operation = request.getRequestType().name(); 412 if (request instanceof ActionRequest) { 413 final String action = ((ActionRequest) request).getAction(); 414 final JsonValue detail = json(object(field("action", action))); 415 request(CREST_PROTOCOL, operation, detail); 416 } else { 417 request(CREST_PROTOCOL, operation); 418 } 419 return self(); 420 } 421 422 /** 423 * Sets common fields from services contexts. 424 * 425 * @param context The services context. 426 * 427 * @see #transactionIdFromContext(Context) 428 * @see #clientFromContext(Context) 429 * @see #serverFromContext(Context) 430 * @see #httpFromContext(Context) 431 * 432 * @return this builder 433 */ 434 public final T forContext(Context context) { 435 transactionIdFromContext(context); 436 clientFromContext(context); 437 serverFromContext(context); 438 httpFromContext(context); 439 return self(); 440 } 441 442 /** 443 * Sets common fields from CREST contexts and request. 444 * 445 * @param context The CREST context. 446 * @param request The CREST request. 447 * 448 * @see #transactionIdFromContext(Context) 449 * @see #clientFromContext(Context) 450 * @see #serverFromContext(Context) 451 * @see #httpFromContext(Context) 452 * @see #requestFromCrestRequest(Request) 453 * 454 * @return this builder 455 */ 456 public final T forHttpRequest(Context context, Request request) { 457 forContext(context); 458 requestFromCrestRequest(request); 459 return self(); 460 } 461 462 /** 463 * The status of the access request. 464 */ 465 public enum ResponseStatus { 466 /** The access request was successfully completed. */ 467 SUCCESSFUL, 468 /** The access request was not successfully completed. */ 469 FAILED 470 } 471}