View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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                 // create sorted set only if it itself is not already SortedSet
112                 if (!SortedSet.class.isAssignableFrom(set.getClass())) {
113                     Object item = set.iterator().next();
114                     if (Comparable.class.isAssignableFrom(item.getClass())) {
115                         // and only if items are Comparable
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         // add also select to choose with which version compare
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                 // change after audit entries in order to match only the ones newer than the current after one
228                 afterVersionsPanel.setChoices(auditEntries.stream().
229                         filter(ae -> ae.getDate().isAfter(beforeEntry.getDate())
230                         || ae.getDate().isEqual(beforeEntry.getDate())).
231                         collect(Collectors.toList()));
232                 // set the new after entry
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         // audit fetch size is fixed, for the moment... 
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         // the default selected is the newest one, if any
302         latestAuditEntry = auditEntries.isEmpty() ? null : auditEntries.get(0);
303         after = latestAuditEntry == null ? null : buildAfterAuditEntry(latestAuditEntry);
304         // add default diff panel
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         // current by default is the output of the selected event
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 }