1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.syncope.client.console.audit;
20
21 import com.fasterxml.jackson.core.JsonGenerator;
22 import com.fasterxml.jackson.core.JsonProcessingException;
23 import com.fasterxml.jackson.core.StreamReadFeature;
24 import com.fasterxml.jackson.databind.JsonNode;
25 import com.fasterxml.jackson.databind.ObjectMapper;
26 import com.fasterxml.jackson.databind.SerializerProvider;
27 import com.fasterxml.jackson.databind.json.JsonMapper;
28 import com.fasterxml.jackson.databind.module.SimpleModule;
29 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
30 import com.fasterxml.jackson.databind.node.ObjectNode;
31 import com.fasterxml.jackson.databind.ser.std.StdSerializer;
32 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
33 import java.io.IOException;
34 import java.io.Serializable;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.Set;
38 import java.util.SortedSet;
39 import java.util.TreeMap;
40 import java.util.TreeSet;
41 import java.util.stream.Collectors;
42 import org.apache.commons.lang3.StringUtils;
43 import org.apache.syncope.client.console.SyncopeConsoleSession;
44 import org.apache.syncope.client.console.rest.AuditRestClient;
45 import org.apache.syncope.client.console.wicket.ajax.form.IndicatorAjaxEventBehavior;
46 import org.apache.syncope.client.console.wicket.markup.html.form.JsonDiffPanel;
47 import org.apache.syncope.client.ui.commons.Constants;
48 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
49 import org.apache.syncope.client.ui.commons.panels.ModalPanel;
50 import org.apache.syncope.common.lib.audit.AuditEntry;
51 import org.apache.syncope.common.lib.to.EntityTO;
52 import org.apache.syncope.common.lib.to.UserTO;
53 import org.apache.syncope.common.lib.types.AuditElements;
54 import org.apache.wicket.WicketRuntimeException;
55 import org.apache.wicket.ajax.AjaxRequestTarget;
56 import org.apache.wicket.ajax.markup.html.AjaxLink;
57 import org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
58 import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
59 import org.apache.wicket.markup.html.form.IChoiceRenderer;
60 import org.apache.wicket.markup.html.panel.Panel;
61 import org.apache.wicket.model.IModel;
62 import org.apache.wicket.model.Model;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66 public abstract class AuditHistoryDetails<T extends Serializable> extends Panel implements ModalPanel {
67
68 private static final long serialVersionUID = -7400543686272100483L;
69
70 protected static final Logger LOG = LoggerFactory.getLogger(AuditHistoryDetails.class);
71
72 public static final List<String> DEFAULT_EVENTS = List.of(
73 "create", "update", "matchingrule_update", "unmatchingrule_assign", "unmatchingrule_provision");
74
75 protected static final SortParam<String> REST_SORT = new SortParam<>("event_date", false);
76
77 protected static class SortingNodeFactory extends JsonNodeFactory {
78
79 private static final long serialVersionUID = 1870252010670L;
80
81 @Override
82 public ObjectNode objectNode() {
83 return new ObjectNode(this, new TreeMap<>());
84 }
85 }
86
87 protected static class SortedSetJsonSerializer extends StdSerializer<Set<?>> {
88
89 private static final long serialVersionUID = 3849059774309L;
90
91 SortedSetJsonSerializer(final Class<Set<?>> clazz) {
92 super(clazz);
93 }
94
95 @Override
96 public void serialize(
97 final Set<?> set,
98 final JsonGenerator gen,
99 final SerializerProvider sp) throws IOException {
100
101 if (set == null) {
102 gen.writeNull();
103 return;
104 }
105
106 gen.writeStartArray();
107
108 if (!set.isEmpty()) {
109 Set<?> sorted = set;
110
111
112 if (!SortedSet.class.isAssignableFrom(set.getClass())) {
113 Object item = set.iterator().next();
114 if (Comparable.class.isAssignableFrom(item.getClass())) {
115
116 sorted = new TreeSet<>(set);
117 } else {
118 LOG.debug("Cannot sort items of type {}", item.getClass());
119 }
120 }
121
122 for (Object item : sorted) {
123 gen.writeObject(item);
124 }
125 }
126
127 gen.writeEndArray();
128 }
129 }
130
131 @SuppressWarnings("unchecked")
132 protected static <T> Class<T> cast(final Class<?> aClass) {
133 return (Class<T>) aClass;
134 }
135
136 protected static final ObjectMapper MAPPER = JsonMapper.builder().
137 nodeFactory(new SortingNodeFactory()).build().
138 registerModule(new SimpleModule().addSerializer(new SortedSetJsonSerializer(cast(Set.class)))).
139 registerModule(new JavaTimeModule());
140
141 protected EntityTO currentEntity;
142
143 protected AuditElements.EventCategoryType type;
144
145 protected String category;
146
147 protected final List<String> events;
148
149 protected Class<T> reference;
150
151 protected final List<AuditEntry> auditEntries = new ArrayList<>();
152
153 protected AuditEntry latestAuditEntry;
154
155 protected AuditEntry after;
156
157 protected AjaxDropDownChoicePanel<AuditEntry> beforeVersionsPanel;
158
159 protected AjaxDropDownChoicePanel<AuditEntry> afterVersionsPanel;
160
161 protected final AjaxLink<Void> restore;
162
163 protected final AuditRestClient restClient;
164
165 @SuppressWarnings("unchecked")
166 public AuditHistoryDetails(
167 final String id,
168 final EntityTO currentEntity,
169 final AuditElements.EventCategoryType type,
170 final String category,
171 final List<String> events,
172 final String auditRestoreEntitlement,
173 final AuditRestClient restClient) {
174
175 super(id);
176
177 this.currentEntity = currentEntity;
178 this.type = type;
179 this.category = category;
180 this.events = events;
181 this.reference = (Class<T>) currentEntity.getClass();
182 this.restClient = restClient;
183
184 setOutputMarkupId(true);
185
186 IChoiceRenderer<AuditEntry> choiceRenderer = new IChoiceRenderer<>() {
187
188 private static final long serialVersionUID = -3724971416312135885L;
189
190 @Override
191 public String getDisplayValue(final AuditEntry value) {
192 return SyncopeConsoleSession.get().getDateFormat().format(value.getDate());
193 }
194
195 @Override
196 public String getIdValue(final AuditEntry value, final int i) {
197 return Long.toString(value.getDate().toInstant().toEpochMilli());
198 }
199
200 @Override
201 public AuditEntry getObject(final String id, final IModel<? extends List<? extends AuditEntry>> choices) {
202 return choices.getObject().stream().
203 filter(c -> StringUtils.isNotBlank(id)
204 && Long.parseLong(id) == c.getDate().toInstant().toEpochMilli()).
205 findFirst().orElse(null);
206 }
207 };
208
209
210 beforeVersionsPanel =
211 new AjaxDropDownChoicePanel<>("beforeVersions", getString("beforeVersions"), new Model<>(), true);
212 beforeVersionsPanel.setChoiceRenderer(choiceRenderer);
213 beforeVersionsPanel.add(new IndicatorAjaxEventBehavior(Constants.ON_CHANGE) {
214
215 private static final long serialVersionUID = -6383712635009760397L;
216
217 @Override
218 protected void onEvent(final AjaxRequestTarget target) {
219 AuditEntry beforeEntry = beforeVersionsPanel.getModelObject() == null
220 ? latestAuditEntry
221 : beforeVersionsPanel.getModelObject();
222 AuditEntry afterEntry = afterVersionsPanel.getModelObject() == null
223 ? after
224 : buildAfterAuditEntry(beforeEntry);
225 AuditHistoryDetails.this.addOrReplace(
226 new JsonDiffPanel(toJSON(beforeEntry, reference), toJSON(afterEntry, reference)));
227
228 afterVersionsPanel.setChoices(auditEntries.stream().
229 filter(ae -> ae.getDate().isAfter(beforeEntry.getDate())
230 || ae.getDate().isEqual(beforeEntry.getDate())).
231 collect(Collectors.toList()));
232
233 afterVersionsPanel.setModelObject(afterEntry);
234 target.add(AuditHistoryDetails.this);
235 }
236 });
237 afterVersionsPanel =
238 new AjaxDropDownChoicePanel<>("afterVersions", getString("afterVersions"), new Model<>(), true);
239 afterVersionsPanel.setChoiceRenderer(choiceRenderer);
240 afterVersionsPanel.add(new IndicatorAjaxEventBehavior(Constants.ON_CHANGE) {
241
242 private static final long serialVersionUID = -6383712635009760397L;
243
244 @Override
245 protected void onEvent(final AjaxRequestTarget target) {
246 AuditHistoryDetails.this.addOrReplace(new JsonDiffPanel(
247 toJSON(beforeVersionsPanel.getModelObject() == null
248 ? latestAuditEntry
249 : beforeVersionsPanel.getModelObject(), reference),
250 toJSON(afterVersionsPanel.getModelObject() == null
251 ? after
252 : buildAfterAuditEntry(afterVersionsPanel.getModelObject()), reference)));
253 target.add(AuditHistoryDetails.this);
254 }
255 });
256 add(beforeVersionsPanel.setOutputMarkupId(true));
257 add(afterVersionsPanel.setOutputMarkupId(true));
258
259 restore = new AjaxLink<>("restore") {
260
261 private static final long serialVersionUID = -817438685948164787L;
262
263 @Override
264 public void onClick(final AjaxRequestTarget target) {
265 try {
266 AuditEntry before = beforeVersionsPanel.getModelObject() == null
267 ? latestAuditEntry
268 : beforeVersionsPanel.getModelObject();
269 String json = before.getBefore() == null
270 ? MAPPER.readTree(before.getOutput()).get("entity") == null
271 ? MAPPER.readTree(before.getOutput()).toPrettyString()
272 : MAPPER.readTree(before.getOutput()).get("entity").toPrettyString()
273 : before.getBefore();
274 restore(json, target);
275 } catch (JsonProcessingException e) {
276 throw new WicketRuntimeException(e);
277 }
278 }
279 };
280 MetaDataRoleAuthorizationStrategy.authorize(restore, ENABLE, auditRestoreEntitlement);
281 add(restore);
282
283 initDiff();
284 }
285
286 protected abstract void restore(String json, AjaxRequestTarget target);
287
288 protected void initDiff() {
289
290 auditEntries.clear();
291 auditEntries.addAll(restClient.search(
292 currentEntity.getKey(),
293 1,
294 50,
295 type,
296 category,
297 events,
298 AuditElements.Result.SUCCESS,
299 REST_SORT));
300
301
302 latestAuditEntry = auditEntries.isEmpty() ? null : auditEntries.get(0);
303 after = latestAuditEntry == null ? null : buildAfterAuditEntry(latestAuditEntry);
304
305 addOrReplace(new JsonDiffPanel(toJSON(latestAuditEntry, reference), toJSON(after, reference)));
306
307 beforeVersionsPanel.setChoices(auditEntries);
308 afterVersionsPanel.setChoices(auditEntries.stream().
309 filter(ae -> ae.getDate().isAfter(after.getDate()) || ae.getDate().isEqual(after.getDate())).
310 collect(Collectors.toList()));
311
312 beforeVersionsPanel.setModelObject(latestAuditEntry);
313 afterVersionsPanel.setModelObject(after);
314
315 restore.setEnabled(!auditEntries.isEmpty());
316 }
317
318 protected AuditEntry buildAfterAuditEntry(final AuditEntry input) {
319 AuditEntry output = new AuditEntry();
320 output.setWho(input.getWho());
321 output.setDate(input.getDate());
322
323 output.setOutput(input.getOutput());
324 output.setThrowable(input.getThrowable());
325 return output;
326 }
327
328 protected Model<String> toJSON(final AuditEntry auditEntry, final Class<T> reference) {
329 if (auditEntry == null) {
330 return Model.of();
331 }
332
333 try {
334 String content;
335 if (auditEntry.getBefore() == null) {
336 JsonNode output = MAPPER.readTree(auditEntry.getOutput());
337 if (output.has("entity")) {
338 content = output.get("entity").toPrettyString();
339 } else {
340 content = output.toPrettyString();
341 }
342 } else {
343 content = auditEntry.getBefore();
344 }
345
346 T entity = MAPPER.reader().
347 with(StreamReadFeature.STRICT_DUPLICATE_DETECTION).
348 readValue(content, reference);
349 if (entity instanceof UserTO) {
350 UserTO userTO = (UserTO) entity;
351 userTO.setPassword(null);
352 userTO.setSecurityAnswer(null);
353 }
354
355 return Model.of(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(entity));
356 } catch (Exception e) {
357 LOG.error("While (de)serializing entity {}", auditEntry, e);
358 return Model.of();
359 }
360 }
361 }