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