1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.forgerock.audit.handlers.json;
18
19 import static org.forgerock.audit.util.JsonValueUtils.JSONVALUE_FILTER_VISITOR;
20 import static org.forgerock.json.JsonValue.*;
21 import static org.forgerock.json.resource.ResourceException.*;
22 import static org.forgerock.json.resource.ResourceResponse.FIELD_CONTENT_ID;
23 import static org.forgerock.json.resource.Responses.*;
24
25 import java.io.BufferedReader;
26 import java.io.IOException;
27 import java.io.InputStreamReader;
28 import java.nio.charset.StandardCharsets;
29 import java.nio.file.Files;
30 import java.nio.file.Path;
31 import java.util.Map;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
34
35 import com.fasterxml.jackson.databind.ObjectMapper;
36 import org.forgerock.audit.events.EventTopicsMetaData;
37 import org.forgerock.audit.events.handlers.AuditEventHandler;
38 import org.forgerock.audit.events.handlers.AuditEventHandlerBase;
39 import org.forgerock.audit.util.ElasticsearchUtil;
40 import org.forgerock.json.JsonPointer;
41 import org.forgerock.json.JsonValue;
42 import org.forgerock.json.resource.ActionRequest;
43 import org.forgerock.json.resource.ActionResponse;
44 import org.forgerock.json.resource.CountPolicy;
45 import org.forgerock.json.resource.QueryRequest;
46 import org.forgerock.json.resource.QueryResourceHandler;
47 import org.forgerock.json.resource.QueryResponse;
48 import org.forgerock.json.resource.ResourceException;
49 import org.forgerock.json.resource.ResourceResponse;
50 import org.forgerock.services.context.Context;
51 import org.forgerock.util.promise.Promise;
52 import org.forgerock.util.query.QueryFilter;
53
54
55
56
57
58
59 public class JsonAuditEventHandler extends AuditEventHandlerBase {
60
61 static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
62
63
64
65
66
67
68
69
70 static final String EVENT_ID_FIELD = "_eventId";
71
72
73
74
75 public static final String ROTATE_FILE_ACTION_NAME = "rotate";
76
77
78
79
80
81 public static final String FLUSH_FILE_ACTION_NAME = "flush";
82
83 private static final String ID_FIELD_PATTERN_PREFIX = "\"" + FIELD_CONTENT_ID + "\"\\s*:\\s*\"";
84 private static final String EVENT_ID_FIELD_PATTERN_PREFIX = "\"" + EVENT_ID_FIELD + "\"\\s*:\\s*\"";
85 private static final String FIELD_PATTERN_SUFFIX = "\"";
86
87 private final JsonFileWriter jsonFileWriter;
88 private final boolean elasticsearchCompatible;
89
90
91
92
93
94
95
96 public JsonAuditEventHandler(
97 final JsonAuditEventHandlerConfiguration configuration,
98 final EventTopicsMetaData eventTopicsMetaData) {
99 super(configuration.getName(), eventTopicsMetaData, configuration.getTopics(), configuration.isEnabled());
100 jsonFileWriter = new JsonFileWriter(configuration.getTopics(), configuration, true);
101 elasticsearchCompatible = configuration.isElasticsearchCompatible();
102 }
103
104 @Override
105 public void startup() throws ResourceException {
106 jsonFileWriter.startup();
107 }
108
109 @Override
110 public void shutdown() throws ResourceException {
111 jsonFileWriter.shutdown();
112 }
113
114 @Override
115 public Promise<ResourceResponse, ResourceException> publishEvent(final Context context, final String topic,
116 final JsonValue event) {
117 try {
118 jsonFileWriter.put(topic, event);
119 } catch (Exception e) {
120 return newResourceException(INTERNAL_ERROR, "Failed to add event to queue", e).asPromise();
121 }
122 return newResourceResponse(event.get(FIELD_CONTENT_ID).asString(), null, event).asPromise();
123 }
124
125 @Override
126 public Promise<ResourceResponse, ResourceException> readEvent(final Context context, final String topic,
127 final String resourceId) {
128 final Path jsonFilePath = jsonFileWriter.getTopicFilePath(topic);
129 if (jsonFilePath == null) {
130 return newResourceException(NOT_FOUND, "Topic not found: " + topic).asPromise();
131 }
132 final String fieldPatternPrefix = elasticsearchCompatible
133 ? EVENT_ID_FIELD_PATTERN_PREFIX : ID_FIELD_PATTERN_PREFIX;
134 final Matcher idMatcher = Pattern.compile(fieldPatternPrefix + resourceId + FIELD_PATTERN_SUFFIX).matcher("");
135 String line;
136 try (final BufferedReader reader = new BufferedReader(new InputStreamReader(
137 Files.newInputStream(jsonFilePath), StandardCharsets.UTF_8))) {
138 line = reader.readLine();
139 while (line != null) {
140 if (idMatcher.reset(line).find()) {
141 final JsonValue event = denormalizeJsonEvent(new JsonValue(
142 OBJECT_MAPPER.readValue(line, Map.class)));
143 return newResourceResponse(resourceId, null, event).asPromise();
144 }
145 line = reader.readLine();
146 }
147 return newResourceException(NOT_FOUND, "Resource not found with ID: " + resourceId).asPromise();
148 } catch (Exception e) {
149 return newResourceException(INTERNAL_ERROR, "Failed to read json file: " + jsonFilePath, e).asPromise();
150 }
151 }
152
153 @Override
154 public Promise<QueryResponse, ResourceException> queryEvents(final Context context, final String topic,
155 final QueryRequest query, final QueryResourceHandler handler) {
156 final Path jsonFilePath = jsonFileWriter.getTopicFilePath(topic);
157 if (jsonFilePath == null) {
158 return newResourceException(NOT_FOUND, "Topic not found: " + topic).asPromise();
159 }
160 final QueryFilter<JsonPointer> queryFilter = query.getQueryFilter();
161 int results = 0;
162 String line;
163 try (final BufferedReader reader = new BufferedReader(new InputStreamReader(
164 Files.newInputStream(jsonFilePath), StandardCharsets.UTF_8))) {
165 line = reader.readLine();
166 while (line != null) {
167 final JsonValue event = denormalizeJsonEvent(new JsonValue(OBJECT_MAPPER.readValue(line, Map.class)));
168 if (queryFilter.accept(JSONVALUE_FILTER_VISITOR, event)) {
169 ++results;
170 final ResourceResponse resourceResponse =
171 newResourceResponse(event.get(FIELD_CONTENT_ID).asString(), null, event);
172 if (!handler.handleResource(resourceResponse)) {
173 break;
174 }
175 }
176 line = reader.readLine();
177 }
178 } catch (Exception e) {
179 return newResourceException(INTERNAL_ERROR, "Failed to read json file: " + jsonFilePath, e).asPromise();
180 }
181 return newQueryResponse(null, CountPolicy.EXACT, results).asPromise();
182 }
183
184 @Override
185 public Promise<ActionResponse, ResourceException> handleAction(final Context context, final String topic,
186 final ActionRequest request) {
187 final Path jsonFilePath = jsonFileWriter.getTopicFilePath(topic);
188 if (jsonFilePath == null) {
189 return newResourceException(NOT_FOUND, "Topic not found: " + topic).asPromise();
190 }
191 try {
192 switch (request.getAction()) {
193 case ROTATE_FILE_ACTION_NAME:
194 if (!jsonFileWriter.rotateFile(topic)) {
195 return newResourceException(BAD_REQUEST, "Rotation not enabled").asPromise();
196 }
197 break;
198 case FLUSH_FILE_ACTION_NAME:
199 jsonFileWriter.flushFileBuffer(topic);
200 break;
201 default:
202 return newResourceException(BAD_REQUEST, "Unsupported action: " + request.getAction()).asPromise();
203 }
204 return newActionResponse(json(object(field("status", "OK")))).asPromise();
205 } catch (Exception e) {
206 return newResourceException(INTERNAL_ERROR, "Failed to invoke action", e).asPromise();
207 }
208 }
209
210
211
212
213
214
215
216
217
218 private JsonValue denormalizeJsonEvent(JsonValue event) throws IOException {
219 if (elasticsearchCompatible) {
220
221 event = ElasticsearchUtil.denormalizeJson(event);
222 ElasticsearchUtil.renameField(event, EVENT_ID_FIELD, FIELD_CONTENT_ID);
223 }
224 return event;
225 }
226 }