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}