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.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                 // create sorted set only if it itself is not already SortedSet
111                 if (!SortedSet.class.isAssignableFrom(set.getClass())) {
112                     Object item = set.iterator().next();
113                     if (Comparable.class.isAssignableFrom(item.getClass())) {
114                         // and only if items are Comparable
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         // add also select to choose with which version compare
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                 // change after audit entries in order to match only the ones newer than the current after one
227                 afterVersionsPanel.setChoices(auditEntries.stream().
228                         filter(ae -> ae.getDate().isAfter(beforeEntry.getDate())
229                         || ae.getDate().isEqual(beforeEntry.getDate())).
230                         collect(Collectors.toList()));
231                 // set the new after entry
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         // audit fetch size is fixed, for the moment... 
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         // the default selected is the newest one, if any
301         latestAuditEntry = auditEntries.isEmpty() ? null : auditEntries.get(0);
302         after = latestAuditEntry == null ? null : buildAfterAuditEntry(latestAuditEntry);
303         // add default diff panel
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         // current by default is the output of the selected event
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 }