This is an automated email from the ASF dual-hosted git repository.

mmoayyed pushed a commit to branch 2_1_X
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/2_1_X by this push:
     new b04fa34  SYNCOPE-1511: History management for admin console UI (#141)
b04fa34 is described below

commit b04fa345655d87e74f07cbdba80b67e7d65ffec6
Author: Misagh Moayyed <mm1...@gmail.com>
AuthorDate: Thu Dec 5 17:28:12 2019 +0400

    SYNCOPE-1511: History management for admin console UI (#141)
    
    * initial draft of audit services
    
    * cntd with audit services design
    
    * add builder methods
    
    * clean up types
    
    * working on build failures; increased fit timeout
    
    * clean up API
    
    * clean up API
    
    * clean up API
    
    * update audit config for tests
    
    * clean up changes after review
    
    * audit tests for groups
    
    * initial pass at wicket
    
    * Merge branch '2_1_X' into SYNCOPE-1511-Wicket
    
    # Conflicts:
    #   
common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AuditQuery.java
    #   core/logic/src/main/java/org/apache/syncope/core/logic/AuditLogic.java
    #   
core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AuditDAO.java
    #   
core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditDAO.java
    #   core/persistence-jpa/src/test/resources/domains/MasterContent.xml
    #   
core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuditDataBinderImpl.java
    #   
core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AuditServiceImpl.java
    #   
fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java
    
    * clean up formatting
    
    * clean up formatting
    
    * clean up formatting
    
    * clean up formatting
    
    * clean up formatting
    
    * clean up formatting
    
    * clean up formatting
    
    * adjustments after code review
    
    * add view functionality to audit history
    
    * added view and restore functionality
    
    * add notes for restoring user based on audit
    
    * fix query syntax after merge
    
    * test restore functionality w/ serialization
    
    * clean up formatting
    
    * re-org api endpoint and audit entitlement for restore
    
    * clean up formatting
    
    * remove non-paged method; fix num of drop-down comparisons
    
    * additional cleanup after review
    
    * work thru issues with 'created' audit entries
    
    * remove audit records from content
    
    * add test case for any objects; clean up wicket panel refs
    
    * add additional audit tests for read/search ops
    
    * clean up audit tests
    
    * clean up audit tests
    
    * fix search query for any objects; specify type
    
    * add license to required files
    
    * clean up tests; wip
    
    * clean up tests
---
 .../console/audit/AuditHistoryDirectoryPanel.java  | 323 +++++++++++++++++++++
 .../client/console/audit/AuditHistoryModal.java    |  54 ++++
 .../client/console/audit/HistoryAuditDetails.java  | 235 +++++++++++++++
 .../syncope/client/console/commons/Constants.java  |   2 +
 .../console/panels/AnyObjectDirectoryPanel.java    |  23 +-
 .../client/console/panels/GroupDirectoryPanel.java |  21 ++
 .../client/console/panels/UserDirectoryPanel.java  |  24 +-
 .../console/rest/AuditHistoryRestClient.java       |  58 ++++
 .../wicket/markup/html/form/ActionLink.java        |   1 +
 .../client/console/audit/AuditHistoryModal.html    |  26 ++
 .../AuditHistoryModal.properties}                  |   5 +-
 .../AuditHistoryModal_it.properties}               |   5 +-
 .../AuditHistoryModal_ja.properties}               |   5 +-
 .../AuditHistoryModal_pt_BR.properties}            |   5 +-
 .../AuditHistoryModal_ru.properties}               |   5 +-
 .../client/console/audit/HistoryAuditDetails.html  |  29 ++
 .../HistoryAuditDetails.properties}                |   5 +-
 .../HistoryAuditDetails_it.properties}             |   5 +-
 .../HistoryAuditDetails_ja.properties}             |   5 +-
 .../HistoryAuditDetails_pt_BR.properties}          |   5 +-
 .../HistoryAuditDetails_ru.properties}             |   5 +-
 ...operties => AnyObjectDirectoryPanel.properties} |   3 +-
 ...rties => AnyObjectDirectoryPanel_it.properties} |   3 +-
 ...rties => AnyObjectDirectoryPanel_ja.properties} |   3 +-
 ...es => AnyObjectDirectoryPanel_pt_BR.properties} |   3 +-
 ...rties => AnyObjectDirectoryPanel_ru.properties} |   3 +-
 .../console/panels/GroupDirectoryPanel.properties  |   1 +
 .../panels/GroupDirectoryPanel_it.properties       |   1 +
 .../panels/GroupDirectoryPanel_ja.properties       |   1 +
 .../panels/GroupDirectoryPanel_pt_BR.properties    |   1 +
 .../panels/GroupDirectoryPanel_ru.properties       |   1 +
 .../console/panels/UserDirectoryPanel.properties   |   1 +
 .../panels/UserDirectoryPanel_it.properties        |   1 +
 .../panels/UserDirectoryPanel_ja.properties        |   1 +
 .../panels/UserDirectoryPanel_pt_BR.properties     |   1 +
 .../panels/UserDirectoryPanel_ru.properties        |   1 +
 .../markup/html/form/ActionsPanel.properties       |   4 +
 .../markup/html/form/ActionsPanel_it.properties    |   4 +
 .../markup/html/form/ActionsPanel_ja.properties    |   4 +
 .../markup/html/form/ActionsPanel_pt_BR.properties |   4 +
 .../markup/html/form/ActionsPanel_ru.properties    |   4 +
 .../src/test/resources/domains/MasterContent.xml   |  25 +-
 .../src/test/resources/domains/MasterContent.xml   |  27 +-
 .../provisioning/api/serialization/POJOHelper.java |  12 +
 .../provisioning/java/DefaultAuditManager.java     |   5 +
 .../java/data/AuditDataBinderImpl.java             |   6 +-
 .../org/apache/syncope/fit/core/AuditITCase.java   |  86 +++++-
 47 files changed, 997 insertions(+), 55 deletions(-)

diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryDirectoryPanel.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryDirectoryPanel.java
new file mode 100644
index 0000000..902576e
--- /dev/null
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryDirectoryPanel.java
@@ -0,0 +1,323 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.audit;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.console.SyncopeConsoleSession;
+import org.apache.syncope.client.console.commons.Constants;
+import org.apache.syncope.client.console.commons.DirectoryDataProvider;
+import org.apache.syncope.client.console.pages.BasePage;
+import org.apache.syncope.client.console.panels.AjaxDataTablePanel;
+import org.apache.syncope.client.console.panels.DirectoryPanel;
+import org.apache.syncope.client.console.panels.ModalPanel;
+import org.apache.syncope.client.console.panels.MultilevelPanel;
+import org.apache.syncope.client.console.rest.AnyObjectRestClient;
+import org.apache.syncope.client.console.rest.AuditHistoryRestClient;
+import org.apache.syncope.client.console.rest.GroupRestClient;
+import org.apache.syncope.client.console.rest.UserRestClient;
+import 
org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.DatePropertyColumn;
+import 
org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
+import org.apache.syncope.common.lib.AnyOperations;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.patch.AnyObjectPatch;
+import org.apache.syncope.common.lib.patch.GroupPatch;
+import org.apache.syncope.common.lib.patch.UserPatch;
+import org.apache.syncope.common.lib.to.AnyObjectTO;
+import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.common.lib.to.AuditEntryTO;
+import org.apache.syncope.common.lib.to.GroupTO;
+import org.apache.syncope.common.lib.to.ProvisioningResult;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.AuditElements;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.StandardEntitlement;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
+import 
org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn;
+import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.StringResourceModel;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class AuditHistoryDirectoryPanel extends
+    DirectoryPanel<AuditEntryTO, AuditEntryTO,
+        AuditHistoryDirectoryPanel.AuditHistoryProvider, 
AuditHistoryRestClient>
+    implements ModalPanel {
+
+    private static final long serialVersionUID = -8248734710505211261L;
+
+    private static final int TOTAL_AUDIT_HISTORY_COMPARISONS = 25;
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private final BaseModal<?> baseModal;
+
+    private final MultilevelPanel multiLevelPanelRef;
+
+    private final AnyTO anyTO;
+
+    private final AnyTypeKind anyTypeKind;
+
+    public AuditHistoryDirectoryPanel(
+        final BaseModal<?> baseModal,
+        final MultilevelPanel multiLevelPanelRef,
+        final PageReference pageRef,
+        final AnyTO anyTO) {
+
+        super(MultilevelPanel.FIRST_LEVEL_ID, pageRef);
+        disableCheckBoxes();
+
+        this.baseModal = baseModal;
+        this.multiLevelPanelRef = multiLevelPanelRef;
+        this.anyTO = anyTO;
+
+        anyTypeKind = AnyTypeKind.fromTOClass(anyTO.getClass());
+        this.restClient = new AuditHistoryRestClient();
+        initResultTable();
+    }
+
+    /**
+     * Restore an object based on the audit record.
+     * <p>
+     * Note that for user objects, the original audit record masks
+     * the password and the security answer; so we cannot use the audit
+     * record to resurrect the entry based on mask data. The method behavior
+     * below will reset the audit record such that the current security answer
+     * and the password for the object are always maintained, and such 
properties
+     * for the user cannot be restored using audit records.
+     *
+     * @param entryBean the entry bean
+     * @param anyTO     the any to
+     * @return the response
+     */
+    private static ProvisioningResult<? extends AnyTO> restore(final 
AuditEntryTO entryBean,
+                                                               final AnyTO 
anyTO) {
+        try {
+            String json = getJSONFromAuditEntry(entryBean);
+            if (anyTO instanceof UserTO) {
+                UserTO userTO = MAPPER.readValue(json, UserTO.class);
+                UserPatch userPatch = AnyOperations.diff(userTO, anyTO, false);
+                userPatch.setPassword(null);
+                userPatch.setSecurityAnswer(null);
+                return new UserRestClient().update(anyTO.getETagValue(), 
userPatch);
+            }
+            if (anyTO instanceof GroupTO) {
+                GroupTO groupTO = MAPPER.readValue(json, GroupTO.class);
+                GroupPatch groupPatch = AnyOperations.diff(groupTO, anyTO, 
false);
+                return new GroupRestClient().update(anyTO.getETagValue(), 
groupPatch);
+            }
+            if (anyTO instanceof AnyObjectTO) {
+                AnyObjectTO anyObjectTO = MAPPER.readValue(json, 
AnyObjectTO.class);
+                AnyObjectPatch anyObjectPatch = 
AnyOperations.diff(anyObjectTO, anyTO, false);
+                return new AnyObjectRestClient().update(anyTO.getETagValue(), 
anyObjectPatch);
+            }
+        } catch (final Exception e) {
+            LOG.error("Could not restore object for {}", anyTO, e);
+        }
+        throw 
SyncopeClientException.build(ClientExceptionType.InvalidAnyObject);
+    }
+
+    private static String getJSONFromAuditEntry(final AuditEntryTO entryBean) 
throws JsonProcessingException {
+        final String json;
+        if (entryBean.getBefore() == null) {
+            json = 
MAPPER.readTree(entryBean.getOutput()).get("entity").toPrettyString();
+        } else {
+            json = entryBean.getBefore();
+        }
+        return json;
+    }
+
+    private static SortParam<String> getSortParam() {
+        return new SortParam<>("event_date", false);
+    }
+
+    private static AuditElements.Result getQueryableAuditResult() {
+        return AuditElements.Result.SUCCESS;
+    }
+
+    private static List<String> getQueryableAuditEvents() {
+        return Arrays.asList("create", "update");
+    }
+
+    @Override
+    protected AuditHistoryDirectoryPanel.AuditHistoryProvider dataProvider() {
+        return new AuditHistoryProvider(rows);
+    }
+
+    @Override
+    protected String paginatorRowsKey() {
+        return Constants.PREF_AUDIT_HISTORY_PAGINATOR_ROWS;
+    }
+
+    @Override
+    protected List<IColumn<AuditEntryTO, String>> getColumns() {
+        final List<IColumn<AuditEntryTO, String>> columns = new ArrayList<>();
+        columns.add(new PropertyColumn<>(
+            new StringResourceModel("who", this), "who"));
+        columns.add(new DatePropertyColumn<>(
+            new StringResourceModel("date", this), null, "date"));
+        return columns;
+    }
+
+    @Override
+    protected void resultTableCustomChanges(
+        final AjaxDataTablePanel.Builder<AuditEntryTO, String> 
resultTableBuilder) {
+        resultTableBuilder.setMultiLevelPanel(baseModal, multiLevelPanelRef);
+    }
+
+    @Override
+    protected ActionsPanel<AuditEntryTO> getActions(final IModel<AuditEntryTO> 
model) {
+        final ActionsPanel<AuditEntryTO> panel = super.getActions(model);
+        final AuditEntryTO auditEntryTO = model.getObject();
+
+        panel.add(new ActionLink<AuditEntryTO>() {
+            private static final long serialVersionUID = -6745431735457245600L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final 
AuditEntryTO modelObject) {
+                AuditHistoryDirectoryPanel.this.getTogglePanel().close(target);
+                viewAuditHistory(modelObject, target);
+                target.add(modal);
+            }
+        }, ActionLink.ActionType.VIEW, StandardEntitlement.AUDIT_READ);
+
+        final String auditRestoreEntitlement;
+        switch (this.anyTypeKind) {
+            case USER:
+                auditRestoreEntitlement = StandardEntitlement.USER_UPDATE;
+                break;
+            case GROUP:
+                auditRestoreEntitlement = StandardEntitlement.GROUP_UPDATE;
+                break;
+            default:
+                auditRestoreEntitlement = StandardEntitlement.ANYTYPE_UPDATE;
+                break;
+        }
+
+        panel.add(new ActionLink<AuditEntryTO>() {
+
+            private static final long serialVersionUID = -6745431735457245600L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final 
AuditEntryTO modelObject) {
+                try {
+                    
AuditHistoryDirectoryPanel.this.getTogglePanel().close(target);
+                    ProvisioningResult<? extends AnyTO> result = 
restore(modelObject, anyTO);
+                    anyTO.setLastChangeDate(new 
Date(Long.parseLong(result.getEntity().getETagValue())));
+                    target.add(container);
+                } catch (SyncopeClientException e) {
+                    LOG.error("While restoring {}", anyTypeKind, e);
+                    
SyncopeConsoleSession.get().error(StringUtils.isBlank(e.getMessage())
+                        ? e.getClass().getName() : e.getMessage());
+                }
+                ((BasePage) 
pageRef.getPage()).getNotificationPanel().refresh(target);
+            }
+        }, ActionLink.ActionType.RESTORE, auditRestoreEntitlement);
+
+        return panel;
+    }
+
+    @Override
+    protected Collection<ActionLink.ActionType> getBatches() {
+        return Collections.emptyList();
+    }
+
+    private void viewAuditHistory(final AuditEntryTO auditEntryBean, final 
AjaxRequestTarget target) {
+        List<AuditEntryTO> search = restClient.search(anyTO.getKey(),
+            0,
+            TOTAL_AUDIT_HISTORY_COMPARISONS,
+            getSortParam(),
+            getQueryableAuditEvents(),
+            getQueryableAuditResult());
+
+        multiLevelPanelRef.next(
+            new StringResourceModel("audit.diff.view", this).getObject(),
+            new HistoryAuditDetails(modal, auditEntryBean,
+                getPage().getPageReference(), toAuditEntryTOs(search), anyTO, 
anyTypeKind), target);
+    }
+
+    private List<AuditEntryTO> toAuditEntryTOs(final List<AuditEntryTO> 
search) {
+        return search
+            .stream()
+            .map(entry -> {
+                AuditEntryTO bean = new AuditEntryTO();
+                bean.setKey(anyTO.getKey());
+                bean.setBefore(entry.getBefore());
+                bean.setDate(entry.getDate());
+                bean.setEvent(entry.getEvent());
+                bean.getInputs().addAll(entry.getInputs());
+                bean.setLoggerName(entry.getLoggerName());
+                bean.setOutput(entry.getOutput());
+                bean.setResult(entry.getResult());
+                bean.setSubCategory(entry.getSubCategory());
+                bean.setThrowable(entry.getThrowable());
+                bean.setWho(entry.getWho());
+                return bean;
+            })
+            .collect(Collectors.toList());
+    }
+
+    protected class AuditHistoryProvider extends 
DirectoryDataProvider<AuditEntryTO> {
+        private static final long serialVersionUID = 415113175628260864L;
+
+        AuditHistoryProvider(final int paginatorRows) {
+            super(paginatorRows);
+        }
+
+        @Override
+        public Iterator<? extends AuditEntryTO> iterator(final long first, 
final long count) {
+            return getAuditEntryBeans(first, count).iterator();
+        }
+
+        @Override
+        public long size() {
+            return restClient.count(anyTO.getKey(), getQueryableAuditEvents(), 
getQueryableAuditResult());
+        }
+
+        @Override
+        public IModel<AuditEntryTO> model(final AuditEntryTO auditEntryBean) {
+            return new CompoundPropertyModel<>(auditEntryBean);
+        }
+
+        private List<AuditEntryTO> getAuditEntryBeans(final long first, final 
long count) {
+            int page = (int) first / paginatorRows;
+            return restClient.search(anyTO.getKey(),
+                Math.max(page, 0) + 1,
+                Long.valueOf(count).intValue(),
+                getSortParam(),
+                getQueryableAuditEvents(),
+                getQueryableAuditResult());
+        }
+    }
+}
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryModal.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryModal.java
new file mode 100644
index 0000000..6b553f1
--- /dev/null
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryModal.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.audit;
+
+import org.apache.syncope.client.console.panels.ModalPanel;
+import org.apache.syncope.client.console.panels.MultilevelPanel;
+import 
org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
+import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.markup.html.panel.Panel;
+
+public class AuditHistoryModal<T extends AnyTO> extends Panel implements 
ModalPanel {
+    private static final long serialVersionUID = 1066124171682570080L;
+
+    protected final AuditHistoryDirectoryPanel directoryPanel;
+
+    public AuditHistoryModal(
+        final BaseModal<?> baseModal,
+        final PageReference pageReference,
+        final T entity) {
+
+        super(BaseModal.CONTENT_ID);
+
+        final MultilevelPanel mlp = new MultilevelPanel("history");
+        mlp.setOutputMarkupId(true);
+        this.directoryPanel = getDirectoryPanel(mlp, baseModal, pageReference, 
entity);
+        add(mlp.setFirstLevel(this.directoryPanel));
+    }
+
+    protected AuditHistoryDirectoryPanel getDirectoryPanel(
+        final MultilevelPanel mlp,
+        final BaseModal<?> baseModal,
+        final PageReference pageReference,
+        final T entity) {
+
+        return new AuditHistoryDirectoryPanel(baseModal, mlp, pageReference, 
entity);
+    }
+}
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/audit/HistoryAuditDetails.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/audit/HistoryAuditDetails.java
new file mode 100644
index 0000000..9628a05
--- /dev/null
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/audit/HistoryAuditDetails.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.audit;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.client.console.SyncopeConsoleSession;
+import org.apache.syncope.client.console.commons.Constants;
+import org.apache.syncope.client.console.panels.AbstractModalPanel;
+import org.apache.syncope.client.console.panels.MultilevelPanel;
+import 
org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
+import 
org.apache.syncope.client.console.wicket.markup.html.form.AjaxDropDownChoicePanel;
+import org.apache.syncope.client.console.wicket.markup.html.form.JsonDiffPanel;
+import 
org.apache.syncope.client.console.wicket.markup.html.form.JsonEditorPanel;
+import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.common.lib.to.AuditEntryTO;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.IChoiceRenderer;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.PropertyModel;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class HistoryAuditDetails extends MultilevelPanel.SecondLevel {
+    private static final String KEY_CURRENT = "current";
+
+    private static final long serialVersionUID = -7400543686272100483L;
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private final AuditEntryTO selected;
+
+    private final List<AuditEntryTO> availableTOs;
+
+    private final AnyTypeKind anyTypeKind;
+
+    private AbstractModalPanel<String> jsonPanel;
+
+    private final AnyTO currentTO;
+
+    public HistoryAuditDetails(final BaseModal<?> baseModal, final 
AuditEntryTO selected,
+                               final PageReference pageRef, final 
List<AuditEntryTO> availableTOs,
+                               final AnyTO currentTO, final AnyTypeKind 
anyTypeKind) {
+        super();
+        this.availableTOs = availableTOs.stream()
+            .filter(object -> !selected.equals(object) && selected.getBefore() 
!= null)
+            .collect(Collectors.toList());
+        this.selected = selected;
+        this.anyTypeKind = anyTypeKind;
+        this.currentTO = currentTO;
+
+        addCurrentInstanceConf();
+        Form<?> form = initDropdownDiffConfForm();
+        add(form);
+        form.setVisible(!this.availableTOs.isEmpty());
+
+        showConfigurationSinglePanel();
+    }
+
+    private void showConfigurationSinglePanel() {
+        Pair<String, String> info = getJSONInfo(selected);
+
+        jsonPanel = new JsonEditorPanel(null, new PropertyModel<>(info, 
"right"), true, null) {
+
+            private static final long serialVersionUID = -8927036362466990179L;
+
+            @Override
+            public void onSubmit(final AjaxRequestTarget target) {
+                modal.close(target);
+            }
+        };
+        jsonPanel.setOutputMarkupId(true);
+
+        addOrReplace(jsonPanel);
+    }
+
+    private void showConfigurationDiffPanel(final List<AuditEntryTO> entries) {
+        List<Pair<String, String>> infos = new ArrayList<>();
+        entries.forEach(entry -> infos.add(getJSONInfo(entry)));
+
+        jsonPanel = new JsonDiffPanel(null, new PropertyModel<>(infos.get(0), 
"value"),
+            new PropertyModel<>(infos.get(1), "value"), null) {
+
+            private static final long serialVersionUID = -8927036362466990179L;
+
+            @Override
+            public void onSubmit(final AjaxRequestTarget target) {
+                modal.close(target);
+            }
+        };
+
+        replace(jsonPanel);
+    }
+
+    private String getSanitizedTOAsJSON(final AnyTO anyTO) throws Exception {
+        if (this.anyTypeKind == AnyTypeKind.USER) {
+            UserTO userTO = (UserTO) anyTO;
+            userTO.setPassword(null);
+            userTO.setSecurityAnswer(null);
+            return 
MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(userTO);
+        }
+        return 
MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(anyTO);
+    }
+
+    private Pair<String, String> getJSONInfo(final AuditEntryTO 
auditEntryBean) {
+        try {
+            final String content;
+            if (auditEntryBean.getBefore() == null) {
+                content = 
MAPPER.readTree(auditEntryBean.getOutput()).get("entity").toPrettyString();
+            } else {
+                content = auditEntryBean.getBefore();
+            }
+
+            AnyTO userTO = MAPPER.readValue(content, anyTypeKind.getTOClass());
+            String json = getSanitizedTOAsJSON(userTO);
+            return Pair.of(auditEntryBean.getKey(), json);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static <T extends AuditEntryTO> Map<String, String> 
getDropdownNamesMap(final List<T> entries) {
+        Map<String, String> map = new LinkedHashMap<>();
+        for (AuditEntryTO audit : entries) {
+            String value = audit.getWho()
+                + " - " + 
SyncopeConsoleSession.get().getDateFormat().format(audit.getDate());
+            if (audit.getKey().equalsIgnoreCase(KEY_CURRENT)) {
+                value += " - " + audit.getKey();
+            }
+            map.put(audit.getKey(), value);
+        }
+        return map;
+    }
+
+    private Form<?> initDropdownDiffConfForm() {
+        final Form<AuditEntryTO> form = new Form<>("form");
+        form.setModel(new CompoundPropertyModel<>(selected));
+        form.setOutputMarkupId(true);
+
+        final Map<String, String> namesMap = getDropdownNamesMap(availableTOs);
+        List<String> keys = new ArrayList<>(namesMap.keySet());
+
+        final AjaxDropDownChoicePanel<String> dropdownElem = new 
AjaxDropDownChoicePanel<>(
+            "compareDropdown",
+            getString("compare"),
+            new PropertyModel<>(selected, "key"),
+            false);
+        dropdownElem.setChoices(keys);
+        dropdownElem.setChoiceRenderer(new IChoiceRenderer<String>() {
+
+            private static final long serialVersionUID = -6265603675261014912L;
+
+            @Override
+            public Object getDisplayValue(final String value) {
+                return namesMap.get(value) == null ? value : 
namesMap.get(value);
+            }
+
+            @Override
+            public String getIdValue(final String value, final int i) {
+                return value;
+            }
+
+            @Override
+            public String getObject(
+                final String id, final IModel<? extends List<? extends 
String>> choices) {
+                return id;
+            }
+        });
+        dropdownElem.setNullValid(false);
+        dropdownElem.getField().add(new 
AjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
+            private static final long serialVersionUID = -1107858522700306810L;
+
+            @Override
+            protected void onUpdate(final AjaxRequestTarget target) {
+                List<AuditEntryTO> elemsToCompare = new ArrayList<>();
+                elemsToCompare.add(selected);
+
+                final String selectedKey = dropdownElem.getModelObject();
+                if (selectedKey != null) {
+                    if (!selectedKey.isEmpty()) {
+                        AuditEntryTO confToCompare = availableTOs.stream().
+                            filter(object -> 
object.getKey().equals(selectedKey)).findAny().orElse(null);
+                        elemsToCompare.add(confToCompare);
+                        showConfigurationDiffPanel(elemsToCompare);
+                    } else {
+                        showConfigurationSinglePanel();
+                    }
+                }
+                target.add(jsonPanel);
+            }
+        });
+        form.add(dropdownElem);
+
+        return form;
+    }
+
+    private void addCurrentInstanceConf() {
+        try {
+            AuditEntryTO entryBean = new AuditEntryTO();
+            entryBean.setKey(KEY_CURRENT);
+            entryBean.setWho(currentTO.getCreator());
+            entryBean.setDate(currentTO.getCreationDate());
+            
entryBean.setBefore(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(currentTO));
+            availableTOs.add(entryBean);
+        } catch (final Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/commons/Constants.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/commons/Constants.java
index e37f8e3..0a2d7b3 100644
--- 
a/client/console/src/main/java/org/apache/syncope/client/console/commons/Constants.java
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/commons/Constants.java
@@ -133,6 +133,8 @@ public final class Constants {
 
     public static final String PREF_RESOURCE_STATUS_PAGINATOR_ROWS = 
"resource.status.paginator.rows";
 
+    public static final String PREF_AUDIT_HISTORY_PAGINATOR_ROWS = 
"audit.history.paginator.rows";
+
     public static final String PREF_CONNECTORS_PAGINATOR_ROWS = 
"connectors.paginator.rows";
 
     public static final String PREF_NOTIFICATION_PAGINATOR_ROWS = 
"notification.paginator.rows";
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel.java
index 34eeed5..229af3d 100644
--- 
a/client/console/src/main/java/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel.java
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel.java
@@ -23,6 +23,7 @@ import java.util.List;
 import org.apache.commons.lang3.SerializationUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
+import org.apache.syncope.client.console.audit.AuditHistoryModal;
 import org.apache.syncope.client.console.commons.Constants;
 import org.apache.syncope.client.console.notifications.NotificationTasks;
 import org.apache.syncope.client.console.pages.BasePage;
@@ -188,7 +189,27 @@ public class AnyObjectDirectoryPanel extends 
AnyDirectoryPanel<AnyObjectTO, AnyO
                 }
             }, ActionType.NOTIFICATION_TASKS, StandardEntitlement.TASK_LIST);
         }
+        panel.add(new ActionLink<AnyObjectTO>() {
+            private static final long serialVersionUID = -2878723352517770644L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final 
AnyObjectTO ignore) {
+                IModel<AnyWrapper<AnyObjectTO>> formModel = new 
CompoundPropertyModel<>(
+                    new AnyWrapper<>(new 
AnyObjectRestClient().read(model.getObject().getKey())));
+                altDefaultModal.setFormModel(formModel);
 
+                target.add(altDefaultModal.setContent(new AuditHistoryModal<>(
+                    altDefaultModal,
+                    pageRef,
+                    formModel.getObject().getInnerObject())));
+
+                altDefaultModal.header(new 
StringResourceModel("auditHistory.title", model));
+
+                altDefaultModal.show(true);
+            }
+        }, ActionType.VIEW_AUDIT_HISTORY, StandardEntitlement.AUDIT_LIST).
+            setRealms(realm, model.getObject().getDynRealms());
+        
         panel.add(new ActionLink<AnyObjectTO>() {
 
             private static final long serialVersionUID = -7978723352517770646L;
@@ -212,7 +233,7 @@ public class AnyObjectDirectoryPanel extends 
AnyDirectoryPanel<AnyObjectTO, AnyO
                 return realm.startsWith(SyncopeConstants.ROOT_REALM);
             }
         }, ActionType.DELETE, AnyEntitlement.DELETE.getFor(type), 
true).setRealm(realm);
-
+        
         return panel;
     }
 
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/panels/GroupDirectoryPanel.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/panels/GroupDirectoryPanel.java
index e7026c2..2556820 100644
--- 
a/client/console/src/main/java/org/apache/syncope/client/console/panels/GroupDirectoryPanel.java
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/panels/GroupDirectoryPanel.java
@@ -25,6 +25,7 @@ import org.apache.commons.lang3.SerializationUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
+import org.apache.syncope.client.console.audit.AuditHistoryModal;
 import org.apache.syncope.client.console.commons.Constants;
 import org.apache.syncope.client.console.layout.FormLayoutInfoUtils;
 import org.apache.syncope.client.console.notifications.NotificationTasks;
@@ -363,6 +364,26 @@ public class GroupDirectoryPanel extends 
AnyDirectoryPanel<GroupTO, GroupRestCli
         }, ActionType.NOTIFICATION_TASKS, StandardEntitlement.TASK_LIST);
 
         panel.add(new ActionLink<GroupTO>() {
+            private static final long serialVersionUID = -2878723352517770644L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final GroupTO 
ignore) {
+                IModel<GroupWrapper> formModel = new CompoundPropertyModel<>(
+                    new GroupWrapper(new 
GroupRestClient().read(model.getObject().getKey())));
+                target.add(altDefaultModal.setContent(new AuditHistoryModal<>(
+                    altDefaultModal,
+                    pageRef,
+                    formModel.getObject().getInnerObject())));
+
+                altDefaultModal.header(new Model<>(
+                    getString("auditHistory.title", new Model<>(new 
AnyWrapper<>(model.getObject())))));
+
+                altDefaultModal.show(true);
+            }
+        }, ActionType.VIEW_AUDIT_HISTORY, StandardEntitlement.AUDIT_LIST).
+            setRealms(realm, model.getObject().getDynRealms());
+
+        panel.add(new ActionLink<GroupTO>() {
 
             private static final long serialVersionUID = -7978723352517770644L;
 
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/panels/UserDirectoryPanel.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/panels/UserDirectoryPanel.java
index 18db27c..d36afbd 100644
--- 
a/client/console/src/main/java/org/apache/syncope/client/console/panels/UserDirectoryPanel.java
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/panels/UserDirectoryPanel.java
@@ -26,6 +26,7 @@ import java.util.List;
 import org.apache.commons.lang3.SerializationUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
+import org.apache.syncope.client.console.audit.AuditHistoryModal;
 import org.apache.syncope.client.console.commons.Constants;
 import org.apache.syncope.client.console.notifications.NotificationTasks;
 import org.apache.syncope.client.console.pages.BasePage;
@@ -144,7 +145,7 @@ public class UserDirectoryPanel extends 
AnyDirectoryPanel<UserTO, UserRestClient
     @Override
     public ActionsPanel<UserTO> getActions(final IModel<UserTO> model) {
         final ActionsPanel<UserTO> panel = super.getActions(model);
-
+         
         panel.add(new ActionLink<UserTO>() {
 
             private static final long serialVersionUID = -7978723352517770644L;
@@ -350,6 +351,27 @@ public class UserDirectoryPanel extends 
AnyDirectoryPanel<UserTO, UserRestClient
 
         panel.add(new ActionLink<UserTO>() {
 
+            private static final long serialVersionUID = -1978723352517770644L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final UserTO 
ignore) {
+                IModel<UserWrapper> formModel = new CompoundPropertyModel<>(
+                    new UserWrapper(new 
UserRestClient().read(model.getObject().getKey())));
+                target.add(altDefaultModal.setContent(new AuditHistoryModal<>(
+                    altDefaultModal,
+                    pageRef,
+                    formModel.getObject().getInnerObject())));
+
+                altDefaultModal.header(new Model<>(
+                    getString("auditHistory.title", new Model<>(new 
AnyWrapper<>(model.getObject())))));
+
+                altDefaultModal.show(true);
+            }
+        }, ActionType.VIEW_AUDIT_HISTORY, StandardEntitlement.AUDIT_LIST).
+            setRealms(realm, model.getObject().getDynRealms());
+
+        panel.add(new ActionLink<UserTO>() {
+
             private static final long serialVersionUID = -7978723352517770644L;
 
             @Override
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/rest/AuditHistoryRestClient.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/rest/AuditHistoryRestClient.java
new file mode 100644
index 0000000..fd3da6a
--- /dev/null
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/rest/AuditHistoryRestClient.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.rest;
+
+import org.apache.syncope.common.lib.to.AuditEntryTO;
+import org.apache.syncope.common.lib.types.AuditElements;
+import org.apache.syncope.common.rest.api.beans.AuditQuery;
+import org.apache.syncope.common.rest.api.service.AuditService;
+import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
+
+import java.util.List;
+
+public class AuditHistoryRestClient extends BaseRestClient {
+    private static final long serialVersionUID = -381814125643246243L;
+
+    public List<AuditEntryTO> search(final String key,
+                                     final int page,
+                                     final int size,
+                                     final SortParam<String> sort,
+                                     final List<String> events,
+                                     final AuditElements.Result result) {
+        AuditQuery query = new AuditQuery.Builder(key)
+            .size(size)
+            .page(page)
+            .events(events)
+            .result(result)
+            .orderBy(toOrderBy(sort))
+            .build();
+        return getService(AuditService.class).search(query).getResult();
+    }
+
+    public int count(final String key,
+                     final List<String> events,
+                     final AuditElements.Result result) {
+        AuditQuery query = new AuditQuery.Builder(key)
+            .events(events)
+            .result(result)
+            .build();
+        return getService(AuditService.class).search(query).getTotalCount();
+    }
+}
+
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLink.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLink.java
index d8dfba0..67afe12 100644
--- 
a/client/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLink.java
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLink.java
@@ -104,6 +104,7 @@ public abstract class ActionLink<T extends Serializable> 
implements Serializable
         VIEW_DETAILS("read"),
         MANAGE_APPROVAL("edit"),
         EDIT_APPROVAL("edit"),
+        VIEW_AUDIT_HISTORY("read"),
         EXTERNAL_EDITOR("externalEditor");
 
         private final String actionId;
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal.html
 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal.html
new file mode 100644
index 0000000..48c38ea
--- /dev/null
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal.html
@@ -0,0 +1,26 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml"; 
xmlns:wicket="http://wicket.apache.org";>
+<wicket:panel>
+  <div class="panel-group box-group">
+    <wicket:child/>
+  </div>
+  <span wicket:id="history">[History]</span>
+</wicket:panel>
+</html>
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal.properties
index 07164b4..e2abe89 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal.properties
@@ -14,5 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+who=Who
+date=Date
+audit.diff.view=Configuration
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal_it.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal_it.properties
index 07164b4..e2abe89 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal_it.properties
@@ -14,5 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+who=Who
+date=Date
+audit.diff.view=Configuration
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal_ja.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal_ja.properties
index 07164b4..e2abe89 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal_ja.properties
@@ -14,5 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+who=Who
+date=Date
+audit.diff.view=Configuration
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal_pt_BR.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal_pt_BR.properties
index 07164b4..e2abe89 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal_pt_BR.properties
@@ -14,5 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+who=Who
+date=Date
+audit.diff.view=Configuration
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal_ru.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal_ru.properties
index 07164b4..e2abe89 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/AuditHistoryModal_ru.properties
@@ -14,5 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+who=Who
+date=Date
+audit.diff.view=Configuration
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails.html
 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails.html
new file mode 100644
index 0000000..da89934
--- /dev/null
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails.html
@@ -0,0 +1,29 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml"; 
xmlns:wicket="http://wicket.apache.org";>
+  <wicket:panel>
+
+    <form wicket:id="form">
+      <span wicket:id="compareDropdown"></span>
+    </form>
+
+    <div wicket:id="content"></div>
+
+  </wicket:panel>
+</html>
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails.properties
index 07164b4..b1406fe 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails.properties
@@ -14,5 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+history.view=History view
+compare=Compare with
+current=Current
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails_it.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails_it.properties
index 07164b4..e128bca 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails_it.properties
@@ -14,5 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+history.view=Storico vista
+compare=paragonare con
+current=presente
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails_ja.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails_ja.properties
index 07164b4..b1406fe 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails_ja.properties
@@ -14,5 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+history.view=History view
+compare=Compare with
+current=Current
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails_pt_BR.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails_pt_BR.properties
index 07164b4..b1406fe 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails_pt_BR.properties
@@ -14,5 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+history.view=History view
+compare=Compare with
+current=Current
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails_ru.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails_ru.properties
index 07164b4..b1406fe 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/audit/HistoryAuditDetails_ru.properties
@@ -14,5 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+history.view=History view
+compare=Compare with
+current=Current
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel.properties
index 07164b4..441f190 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel.properties
@@ -14,5 +14,4 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+auditHistory.title=${type} ${name} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel_it.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel_it.properties
index 07164b4..441f190 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel_it.properties
@@ -14,5 +14,4 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+auditHistory.title=${type} ${name} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel_ja.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel_ja.properties
index 07164b4..441f190 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel_ja.properties
@@ -14,5 +14,4 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+auditHistory.title=${type} ${name} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel_pt_BR.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel_pt_BR.properties
index 07164b4..441f190 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel_pt_BR.properties
@@ -14,5 +14,4 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+auditHistory.title=${type} ${name} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel_ru.properties
similarity index 89%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
copy to 
client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel_ru.properties
index 07164b4..441f190 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/AnyObjectDirectoryPanel_ru.properties
@@ -14,5 +14,4 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-any.edit=Edit ${anyTO.type} ${anyTO.name}
-group.members=${right} members of ${left.name}
+auditHistory.title=${type} ${name} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
index 07164b4..7c1cb8c 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel.properties
@@ -16,3 +16,4 @@
 # under the License.
 any.edit=Edit ${anyTO.type} ${anyTO.name}
 group.members=${right} members of ${left.name}
+auditHistory.title=${anyTO.type} ${anyTO.name} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_it.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_it.properties
index bd97397..8566276 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_it.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_it.properties
@@ -16,3 +16,4 @@
 # under the License.
 any.edit=Modifica ${anyTO.type} ${anyTO.name}
 group.members=Membri ${right} di '${left.name}'
+auditHistory.title=${anyTO.type} ${anyTO.name} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_ja.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_ja.properties
index 0267d65..2f0982a 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_ja.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_ja.properties
@@ -16,3 +16,4 @@
 # under the License.
 any.edit=${anyTO.type} ${anyTO.name} \u3092\u7de8\u96c6
 group.members=${left.name} \u306e ${right} \u30e1\u30f3\u30d0\u30fc 
+auditHistory.title=${anyTO.type} ${anyTO.name} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_pt_BR.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_pt_BR.properties
index 9fb316d..b5730e2 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_pt_BR.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_pt_BR.properties
@@ -16,3 +16,4 @@
 # under the License.
 any.edit=Alterar ${anyTO.type} ${anyTO.name}
 group.members=${right} members of ${left.name}
+auditHistory.title=${anyTO.type} ${anyTO.name} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_ru.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_ru.properties
index 94a1bd9..84e3f7b 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_ru.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/GroupDirectoryPanel_ru.properties
@@ -17,3 +17,4 @@
 #
 any.edit=\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c ${anyTO.type} 
${anyTO.name}
 group.members=${right} \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 
${left.name}
+auditHistory.title=${anyTO.type} ${anyTO.name} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel.properties
index 6acf5db..1d4db0d 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel.properties
@@ -18,3 +18,4 @@ any.edit=Edit ${anyTO.type} ${anyTO.username}
 any.propagation.tasks=Propagation tasks for ${type} ${username}
 any.notification.tasks=Notification tasks for ${type} ${username}
 linkedAccounts.title=Manage user accounts
+auditHistory.title=${anyTO.type} ${anyTO.username} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_it.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_it.properties
index 38fa14d..ef6c91a 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_it.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_it.properties
@@ -18,3 +18,4 @@ any.edit=Modifica ${anyTO.type} ${anyTO.username}
 any.propagation.tasks=Task di propagazione per ${type} ${username}
 any.notification.tasks=Task di notifica per ${type} ${username}
 linkedAccounts.title=Gestisci account utente
+auditHistory.title=${anyTO.type} ${anyTO.username} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ja.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ja.properties
index 9107368..a2193e2 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ja.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ja.properties
@@ -18,3 +18,4 @@ any.edit=${anyTO.type} ${anyTO.username} \u3092\u7de8\u96c6
 any.propagation.tasks=${type} ${username} \u306e\u4f1d\u64ad\u30bf\u30b9\u30af
 any.notification.tasks=${type} ${username} \u306e\u901a\u77e5\u30bf\u30b9\u30af
 
linkedAccounts.title=\u30e6\u30fc\u30b6\u30fc\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u7ba1\u7406\u3059\u308b
+auditHistory.title=${anyTO.type} ${anyTO.username} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_pt_BR.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_pt_BR.properties
index 865e74d..84d5d86 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_pt_BR.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_pt_BR.properties
@@ -18,3 +18,4 @@ any.edit=Alterar ${anyTO.type} ${anyTO.username}
 any.propagation.tasks=Propagation tasks for ${type} ${username}
 any.notification.tasks=Notification tasks for ${type} ${username}
 linkedAccounts.title=Gerenciar contas de usu\u00e1rio
+auditHistory.title=${anyTO.type} ${anyTO.username} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ru.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ru.properties
index 53e0e86..378c49a 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ru.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/panels/UserDirectoryPanel_ru.properties
@@ -19,3 +19,4 @@ any.edit=\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c 
${anyTO.type} ${anyTO.
 any.propagation.tasks=\u0417\u0430\u0434\u0430\u0447\u0438 
\u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f 
\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0434\u043b\u044f ${type} 
${username}
 any.notification.tasks=\u0417\u0430\u0434\u0430\u0447\u0438 
\u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 
\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439 
\u0434\u043b\u044f ${type} ${username}
 
linkedAccounts.title=\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435
 \u0443\u0447\u0435\u0442\u043d\u044b\u043c\u0438 
\u0437\u0430\u043f\u0438\u0441\u044f\u043c\u0438 
\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439
+auditHistory.title=${anyTO.type} ${anyTO.username} history
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
index 333adca..be4d4d1 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel.properties
@@ -280,3 +280,7 @@ manage_accounts.alt=manage accounts icon
 workflow_modeler.class=fa fa-picture-o
 workflow_modeler.title=workflow modeler
 workflow_modeler.alt=workflow modeler icon
+
+view_audit_history.class=fa fa-history
+view_audit_history.title=history management
+view_audit_history.alt=history management icon
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_it.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_it.properties
index 760b241..fe1457a 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_it.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_it.properties
@@ -275,3 +275,7 @@ reconciliation_pull.alt=reconciliation pull icon
 manage_accounts.class=fa fa-users
 manage_accounts.title=gestisci account
 manage_accounts.alt=manage accounts icon
+
+view_audit_history.class=fa fa-history
+view_audit_history.title=storico modifiche
+view_audit_history.alt=storico modifiche icon
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ja.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ja.properties
index e2c053a..e23c35e 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ja.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ja.properties
@@ -277,3 +277,7 @@ reconciliation_pull.alt=\u7167\u5408\u30d7\u30eb icon
 manage_accounts.class=fa fa-users
 
manage_accounts.title=\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u7ba1\u7406\u3059\u308b
 
manage_accounts.alt=\u30a2\u30ab\u30a6\u30f3\u30c8\u7ba1\u7406\u30a2\u30a4\u30b3\u30f3
+
+view_audit_history.class=fa fa-history
+view_audit_history.title=history management
+view_audit_history.alt=history management icon
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_pt_BR.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_pt_BR.properties
index 184d6b7..9ca5a2a 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_pt_BR.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_pt_BR.properties
@@ -282,3 +282,7 @@ reconciliation_pull.alt=reconciliation pull icon
 manage_accounts.class=fa fa-users
 manage_accounts.title=manage accounts
 manage_accounts.alt=manage accounts icon
+
+view_audit_history.class=fa fa-history
+view_audit_history.title=history management
+view_audit_history.alt=history management icon
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ru.properties
 
b/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ru.properties
index d3ef6cc..e3ac79b 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ru.properties
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/wicket/markup/html/form/ActionsPanel_ru.properties
@@ -276,3 +276,7 @@ reconciliation_pull.alt=reconciliation pull icon
 manage_accounts.class=fa fa-users
 manage_accounts.title=manage accounts
 manage_accounts.alt=manage accounts icon
+
+view_audit_history.class=fa fa-history
+view_audit_history.title=history management
+view_audit_history.alt=history management icon
diff --git 
a/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml 
b/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml
index d94cc30..5469e67 100644
--- a/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml
+++ b/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml
@@ -2410,6 +2410,25 @@ $$ }&#10;
 
   <SyncopeLogger 
logName="syncope.audit.[LOGIC]:[SyncopeLogic]:[]:[isSelfRegAllowed]:[SUCCESS]" 
logLevel="DEBUG" logType="AUDIT"/>
 
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[assign]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[assign]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[create]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[create]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[delete]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[delete]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[deprovision]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[deprovision]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[link]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[link]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[provision]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[provision]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[unassign]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[unassign]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[unlink]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[unlink]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[update]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[update]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[assign]:[FAILURE]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[assign]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[confirmPasswordReset]:[FAILURE]" 
logLevel="DEBUG"/>
@@ -2426,12 +2445,8 @@ $$ }&#10;
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[mustChangePassword]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[provision]:[FAILURE]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[provision]:[SUCCESS]" 
logLevel="DEBUG"/>
-  <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[read]:[FAILURE]" 
logLevel="DEBUG"/>
-  <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[read]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[requestPasswordReset]:[FAILURE]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[requestPasswordReset]:[SUCCESS]" 
logLevel="DEBUG"/>
-  <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[search]:[FAILURE]" 
logLevel="DEBUG"/>
-  <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[search]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfCreate]:[FAILURE]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfCreate]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfDelete]:[FAILURE]" 
logLevel="DEBUG"/>
@@ -2459,8 +2474,6 @@ $$ }&#10;
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[own]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[provisionMembers]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[provision]:[SUCCESS]" 
logLevel="DEBUG"/>
-  <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[read]:[SUCCESS]" 
logLevel="DEBUG"/>
-  <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[search]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[unassign]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[unlink]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[update]:[SUCCESS]" 
logLevel="DEBUG"/>
diff --git a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml 
b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
index 76000f1..b04b552 100644
--- a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
+++ b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
@@ -2561,7 +2561,26 @@ $$ }&#10;
   <SecurityQuestion id="887028ea-66fc-41e7-b397-620d7ea6dfbb" content="What's 
your mother's maiden name?"/>
 
   <SyncopeLogger 
logName="syncope.audit.[LOGIC]:[SyncopeLogic]:[]:[isSelfRegAllowed]:[SUCCESS]" 
logLevel="DEBUG" logType="AUDIT"/>
-  
+
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[assign]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[assign]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[create]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[create]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[delete]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[delete]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[deprovision]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[deprovision]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[link]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[link]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[provision]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[provision]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[unassign]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[unassign]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[unlink]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[unlink]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[update]:[FAILURE]" 
LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" 
LOGNAME="syncope.audit.[LOGIC]:[AnyObjectLogic]:[]:[update]:[SUCCESS]" 
LOGLEVEL="DEBUG"/>
+
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[assign]:[FAILURE]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[assign]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[confirmPasswordReset]:[FAILURE]" 
logLevel="DEBUG"/>
@@ -2578,12 +2597,8 @@ $$ }&#10;
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[mustChangePassword]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[provision]:[FAILURE]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[provision]:[SUCCESS]" 
logLevel="DEBUG"/>
-  <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[read]:[FAILURE]" 
logLevel="DEBUG"/>
-  <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[read]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[requestPasswordReset]:[FAILURE]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[requestPasswordReset]:[SUCCESS]" 
logLevel="DEBUG"/>
-  <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[search]:[FAILURE]" 
logLevel="DEBUG"/>
-  <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[search]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfCreate]:[FAILURE]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfCreate]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfDelete]:[FAILURE]" 
logLevel="DEBUG"/>
@@ -2611,8 +2626,6 @@ $$ }&#10;
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[own]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[provisionMembers]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[provision]:[SUCCESS]" 
logLevel="DEBUG"/>
-  <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[read]:[SUCCESS]" 
logLevel="DEBUG"/>
-  <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[search]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[unassign]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[unlink]:[SUCCESS]" 
logLevel="DEBUG"/>
   <SyncopeLogger logType="AUDIT" 
logName="syncope.audit.[LOGIC]:[GroupLogic]:[]:[update]:[SUCCESS]" 
logLevel="DEBUG"/>
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/serialization/POJOHelper.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/serialization/POJOHelper.java
index af99906..c7b8faa 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/serialization/POJOHelper.java
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/serialization/POJOHelper.java
@@ -64,6 +64,18 @@ public final class POJOHelper {
         return result;
     }
 
+    public static String serializeWithDefaultPrettyPrinter(final Object 
object) {
+        String result = null;
+
+        try {
+            result = 
MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(object);
+        } catch (Exception e) {
+            LOG.error("During serialization", e);
+        }
+
+        return result;
+    }
+
     public static <T extends Object> T deserialize(final String serialized, 
final Class<T> reference) {
         T result = null;
 
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultAuditManager.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultAuditManager.java
index d5b9df6..374c717 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultAuditManager.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultAuditManager.java
@@ -35,6 +35,8 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.util.Date;
+
 @Transactional(readOnly = true)
 public class DefaultAuditManager implements AuditManager {
 
@@ -52,6 +54,7 @@ public class DefaultAuditManager implements AuditManager {
         AuditEntry auditEntry = AuditEntryImpl.builder().
                 who(who).
                 logger(new AuditLoggerName(type, category, subcategory, event, 
Result.SUCCESS)).
+                date(new Date()).
                 build();
         org.apache.syncope.core.persistence.api.entity.Logger syncopeLogger =
                 loggerDAO.find(auditEntry.getLogger().toLoggerName());
@@ -64,6 +67,7 @@ public class DefaultAuditManager implements AuditManager {
         auditEntry = AuditEntryImpl.builder()
                 .who(who)
                 .logger(new AuditLoggerName(type, category, subcategory, 
event, Result.FAILURE))
+                .date(new Date())
                 .build();
         syncopeLogger = loggerDAO.find(auditEntry.getLogger().toLoggerName());
         auditRequested = syncopeLogger != null && syncopeLogger.getLevel() == 
LoggerLevel.DEBUG;
@@ -110,6 +114,7 @@ public class DefaultAuditManager implements AuditManager {
                 before(before).
                 output(throwable == null ? output : throwable.getMessage()).
                 input(input).
+                date(new Date()).
                 build();
 
         org.apache.syncope.core.persistence.api.entity.Logger syncopeLogger =
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuditDataBinderImpl.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuditDataBinderImpl.java
index de49702..4937503 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuditDataBinderImpl.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuditDataBinderImpl.java
@@ -47,18 +47,18 @@ public class AuditDataBinderImpl implements AuditDataBinder 
{
         }
 
         if (auditEntry.getBefore() != null) {
-            
auditEntryTO.setBefore(POJOHelper.serialize(auditEntry.getBefore()));
+            
auditEntryTO.setBefore(POJOHelper.serializeWithDefaultPrettyPrinter(auditEntry.getBefore()));
         }
 
         if (auditEntry.getInput() != null) {
             
auditEntryTO.getInputs().addAll(Arrays.stream(auditEntry.getInput()).
                     filter(Objects::nonNull).
-                    map(POJOHelper::serialize).
+                    map(POJOHelper::serializeWithDefaultPrettyPrinter).
                     collect(Collectors.toList()));
         }
 
         if (auditEntry.getOutput() != null) {
-            
auditEntryTO.setOutput(POJOHelper.serialize(auditEntry.getOutput()));
+            
auditEntryTO.setOutput(POJOHelper.serializeWithDefaultPrettyPrinter(auditEntry.getOutput()));
         }
 
         return auditEntryTO;
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java
index 3b8b995..f4f075b 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java
@@ -18,23 +18,33 @@
  */
 package org.apache.syncope.fit.core;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.util.Collections;
 import java.util.List;
+
+import org.apache.syncope.client.lib.SyncopeClient;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.to.AnyObjectTO;
 import org.apache.syncope.common.lib.to.AuditEntryTO;
 import org.apache.syncope.common.lib.to.GroupTO;
+import org.apache.syncope.common.lib.to.PagedResult;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.AuditElements;
+import org.apache.syncope.common.rest.api.beans.AnyQuery;
 import org.apache.syncope.common.rest.api.beans.AuditQuery;
 import org.apache.syncope.fit.AbstractITCase;
 import org.junit.jupiter.api.Test;
 
 public class AuditITCase extends AbstractITCase {
+    private static final int MAX_WAIT_SECONDS = 30;
 
-    private AuditEntryTO query(final AuditQuery query, final int 
maxWaitSeconds) {
+    private static AuditEntryTO query(final AuditQuery query, final int 
maxWaitSeconds, final boolean failIfEmpty) {
         int i = 0;
         List<AuditEntryTO> results = Collections.emptyList();
         do {
@@ -48,20 +58,40 @@ public class AuditITCase extends AbstractITCase {
             i++;
         } while (results.isEmpty() && i < maxWaitSeconds);
         if (results.isEmpty()) {
-            fail("Timeout when executing query for key " + 
query.getEntityKey());
+            if (failIfEmpty) {
+                fail("Timeout when executing query for key " + 
query.getEntityKey());
+            }
+            return null;
         }
 
         return results.get(0);
     }
 
     @Test
+    public void userReadAndSearchYieldsNoAudit() {
+        PagedResult<UserTO> users = userService.search(
+            new 
AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).page(1).size(2).build());
+        assertNotNull(users);
+        assertFalse(users.getResult().isEmpty());
+       
+        for (UserTO user : users.getResult()) {
+            assertNotNull(userService.read(user.getKey()));
+        }
+        for (UserTO user : users.getResult()) {
+            AuditQuery query = new AuditQuery.Builder(user.getKey()).build();
+            AuditEntryTO entry = query(query, MAX_WAIT_SECONDS, false);
+            assertNull(entry);
+        }
+    }
+
+    @Test
     public void findByUser() {
         UserTO userTO = 
createUser(UserITCase.getUniqueSampleTO("au...@syncope.org")).getEntity();
         assertNotNull(userTO.getKey());
 
         AuditQuery query = new 
AuditQuery.Builder(userTO.getKey()).orderBy("event_date desc").
                 page(1).size(1).build();
-        AuditEntryTO entry = query(query, 50);
+        AuditEntryTO entry = query(query, MAX_WAIT_SECONDS, true);
         assertEquals(userTO.getKey(), entry.getKey());
         userService.delete(userTO.getKey());
     }
@@ -80,7 +110,7 @@ public class AuditITCase extends AbstractITCase {
                 event("create").
                 result(AuditElements.Result.SUCCESS).
                 build();
-        AuditEntryTO entry = query(query, 50);
+        AuditEntryTO entry = query(query, MAX_WAIT_SECONDS, true);
         assertEquals(userTO.getKey(), entry.getKey());
         userService.delete(userTO.getKey());
     }
@@ -92,8 +122,50 @@ public class AuditITCase extends AbstractITCase {
 
         AuditQuery query = new 
AuditQuery.Builder(groupTO.getKey()).orderBy("event_date desc").
                 page(1).size(1).build();
-        AuditEntryTO entry = query(query, 50);
+        AuditEntryTO entry = query(query, MAX_WAIT_SECONDS, true);
         assertEquals(groupTO.getKey(), entry.getKey());
         groupService.delete(groupTO.getKey());
     }
+
+    @Test
+    public void groupReadAndSearchYieldsNoAudit() {
+        PagedResult<GroupTO> groups = groupService.search(
+            new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).build());
+        assertNotNull(groups);
+        assertFalse(groups.getResult().isEmpty());
+        for (GroupTO groupTO : groups.getResult()) {
+            assertNotNull(groupService.read(groupTO.getKey()));
+        }
+        for (GroupTO groupTO : groups.getResult()) {
+            AuditQuery query = new 
AuditQuery.Builder(groupTO.getKey()).build();
+            AuditEntryTO entry = query(query, MAX_WAIT_SECONDS, false);
+            assertNull(entry);
+        }
+    }
+
+    @Test
+    public void findByAnyObject() {
+        AnyObjectTO anyObjectTO = 
createAnyObject(AnyObjectITCase.getSampleTO("Italy")).getEntity();
+        assertNotNull(anyObjectTO.getKey());
+        AuditQuery query = new 
AuditQuery.Builder(anyObjectTO.getKey()).orderBy("event_date desc").
+            page(1).size(1).build();
+        AuditEntryTO entry = query(query, MAX_WAIT_SECONDS, true);
+        assertEquals(anyObjectTO.getKey(), entry.getKey());
+        anyObjectService.delete(anyObjectTO.getKey());
+    }
+
+    @Test
+    public void anyObjectReadAndSearchYieldsNoAudit() {
+        PagedResult<AnyObjectTO> anyObjects = anyObjectService.search(
+            new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
+                
fiql(SyncopeClient.getAnyObjectSearchConditionBuilder(getUUIDString()).query()).
+                build());
+        assertNotNull(anyObjects);
+        assertTrue(anyObjects.getResult().isEmpty());
+        for (AnyObjectTO anyObjectTO : anyObjects.getResult()) {
+            AuditQuery query = new 
AuditQuery.Builder(anyObjectTO.getKey()).build();
+            AuditEntryTO entry = query(query, MAX_WAIT_SECONDS, false);
+            assertNull(entry);
+        }
+    }
 }

Reply via email to