You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by mm...@apache.org on 2019/11/27 14:43:22 UTC

[syncope] branch 2_1_X updated: SYNCOPE-1511: Provide Audit APIs (#139)

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 9b816eb  SYNCOPE-1511: Provide Audit APIs (#139)
9b816eb is described below

commit 9b816eb9a954a6238ce0bbb93b9d60c643b4aef1
Author: Misagh Moayyed <mm...@gmail.com>
AuthorDate: Wed Nov 27 18:43:15 2019 +0400

    SYNCOPE-1511: Provide Audit APIs (#139)
    
    * 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
---
 .../apache/syncope/common/lib/to/AuditEntryTO.java | 142 +++++++++++++++++++
 .../common/lib/types/StandardEntitlement.java      |   2 +
 .../syncope/common/rest/api/beans/AuditQuery.java  |  51 +++++++
 .../common/rest/api/service/AuditService.java      |  54 ++++++++
 .../org/apache/syncope/core/logic/AuditLogic.java  |  66 +++++++++
 .../syncope/core/persistence/api/dao/AuditDAO.java |  30 +++++
 .../core/persistence/api/entity/AuditEntry.java    |  43 ++++++
 .../core/persistence/jpa/dao/JPAAuditDAO.java      | 108 +++++++++++++++
 .../src/test/resources/domains/MasterContent.xml   |  56 ++++++++
 .../core/provisioning/api/AuditEntryImpl.java}     | 133 ++++++++++++++++--
 .../provisioning/api/data/AuditDataBinder.java     |  28 ++++
 .../provisioning/java/DefaultAuditManager.java     |  37 +++--
 .../java/data/AuditDataBinderImpl.java             |  57 ++++++++
 .../java/job/report/AuditReportlet.java            |   5 +-
 .../core/rest/cxf/service/AbstractAnyService.java  | 150 ++++++++++-----------
 .../core/rest/cxf/service/AuditServiceImpl.java    |  48 +++++++
 fit/core-reference/pom.xml                         |   2 +-
 .../org/apache/syncope/fit/AbstractITCase.java     |   4 +
 .../org/apache/syncope/fit/core/AuditITCase.java   | 108 +++++++++++++++
 19 files changed, 1017 insertions(+), 107 deletions(-)

diff --git a/common/lib/src/main/java/org/apache/syncope/common/lib/to/AuditEntryTO.java b/common/lib/src/main/java/org/apache/syncope/common/lib/to/AuditEntryTO.java
new file mode 100644
index 0000000..fbaf0ca
--- /dev/null
+++ b/common/lib/src/main/java/org/apache/syncope/common/lib/to/AuditEntryTO.java
@@ -0,0 +1,142 @@
+/*
+ * 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.common.lib.to;
+
+import org.apache.syncope.common.lib.BaseBean;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+@XmlRootElement(name = "audit")
+@XmlType
+public class AuditEntryTO extends BaseBean implements EntityTO  {
+    private static final long serialVersionUID = 1215115961911228005L;
+
+    private final List<String> inputs = new ArrayList<>();
+
+    private String who;
+
+    private String subCategory;
+
+    private String event;
+
+    private String result;
+
+    private String before;
+
+    private String output;
+
+    private Date date;
+
+    private String throwable;
+
+    private String key;
+
+    private String loggerName;
+
+    public Date getDate() {
+        return date;
+    }
+
+    public void setDate(final Date date) {
+        this.date = date;
+    }
+
+    public String getThrowable() {
+        return throwable;
+    }
+
+    public void setThrowable(final String throwable) {
+        this.throwable = throwable;
+    }
+
+    public String getOutput() {
+        return output;
+    }
+
+    public void setOutput(final String output) {
+        this.output = output;
+    }
+
+    public String getBefore() {
+        return before;
+    }
+
+    public void setBefore(final String before) {
+        this.before = before;
+    }
+
+    public List<String> getInputs() {
+        return inputs;
+    }
+
+    public String getSubCategory() {
+        return subCategory;
+    }
+
+    public void setSubCategory(final String subCategory) {
+        this.subCategory = subCategory;
+    }
+
+    public String getEvent() {
+        return event;
+    }
+
+    public void setEvent(final String event) {
+        this.event = event;
+    }
+
+    public String getResult() {
+        return result;
+    }
+
+    public void setResult(final String result) {
+        this.result = result;
+    }
+
+    public String getWho() {
+        return who;
+    }
+
+    public void setWho(final String who) {
+        this.who = who;
+    }
+
+    public String getLoggerName() {
+        return loggerName;
+    }
+
+    public void setLoggerName(final String loggerName) {
+        this.loggerName = loggerName;
+    }
+
+    @Override
+    public String getKey() {
+        return this.key;
+    }
+
+    @Override
+    public void setKey(final String key) {
+        this.key = key;
+    }
+}
diff --git a/common/lib/src/main/java/org/apache/syncope/common/lib/types/StandardEntitlement.java b/common/lib/src/main/java/org/apache/syncope/common/lib/types/StandardEntitlement.java
index 3ca55a3..3d7bcf9 100644
--- a/common/lib/src/main/java/org/apache/syncope/common/lib/types/StandardEntitlement.java
+++ b/common/lib/src/main/java/org/apache/syncope/common/lib/types/StandardEntitlement.java
@@ -110,6 +110,8 @@ public final class StandardEntitlement {
 
     public static final String SCHEMA_DELETE = "SCHEMA_DELETE";
 
+    public static final String AUDIT_SEARCH = "AUDIT_SEARCH";
+
     public static final String USER_SEARCH = "USER_SEARCH";
 
     public static final String USER_CREATE = "USER_CREATE";
diff --git a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AuditQuery.java b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AuditQuery.java
new file mode 100644
index 0000000..d8152de
--- /dev/null
+++ b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AuditQuery.java
@@ -0,0 +1,51 @@
+/*
+ * 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.common.rest.api.beans;
+
+import javax.ws.rs.QueryParam;
+
+public class AuditQuery extends AbstractQuery {
+
+    private static final long serialVersionUID = -2863334226169614417L;
+
+    private String key;
+
+    public String getKey() {
+        return key;
+    }
+
+    @QueryParam("key")
+    public void setKey(final String key) {
+        this.key = key;
+    }
+
+    public static class Builder extends AbstractQuery.Builder<AuditQuery, Builder> {
+
+        public Builder key(final String keyword) {
+            getInstance().setKey(keyword);
+            return this;
+        }
+
+        @Override
+        protected AuditQuery newInstance() {
+            return new AuditQuery();
+        }
+    }
+
+}
diff --git a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuditService.java b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuditService.java
new file mode 100644
index 0000000..897131e
--- /dev/null
+++ b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuditService.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.common.rest.api.service;
+
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.security.SecurityRequirements;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.syncope.common.lib.to.AuditEntryTO;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.beans.AuditQuery;
+
+import javax.ws.rs.BeanParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+/**
+ * REST operations for audit events.
+ */
+@Tag(name = "Audits")
+@SecurityRequirements({
+    @SecurityRequirement(name = "BasicAuthentication"),
+    @SecurityRequirement(name = "Bearer")})
+@Path("audits")
+public interface AuditService {
+
+    /**
+     * Returns a paged list of audit objects matching the given query.
+     *
+     * @param auditQuery query conditions
+     * @return paged list of objects matching the given query
+     */
+    @GET
+    @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML})
+    PagedResult<AuditEntryTO> search(@BeanParam AuditQuery auditQuery);
+}
diff --git a/core/logic/src/main/java/org/apache/syncope/core/logic/AuditLogic.java b/core/logic/src/main/java/org/apache/syncope/core/logic/AuditLogic.java
new file mode 100644
index 0000000..30ef3a2
--- /dev/null
+++ b/core/logic/src/main/java/org/apache/syncope/core/logic/AuditLogic.java
@@ -0,0 +1,66 @@
+/*
+ * 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.core.logic;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.to.AuditEntryTO;
+import org.apache.syncope.common.lib.types.StandardEntitlement;
+import org.apache.syncope.core.persistence.api.dao.AuditDAO;
+import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
+import org.apache.syncope.core.persistence.api.entity.AuditEntry;
+import org.apache.syncope.core.provisioning.api.data.AuditDataBinder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Component
+public class AuditLogic extends AbstractTransactionalLogic<AuditEntryTO> {
+    @Autowired
+    private AuditDataBinder binder;
+
+    @Autowired
+    private AuditDAO auditDAO;
+
+    @Override
+    protected AuditEntryTO resolveReference(final Method method, final Object... args)
+        throws UnresolvedReferenceException {
+        throw new UnresolvedReferenceException();
+    }
+
+    @PreAuthorize("hasRole('" + StandardEntitlement.AUDIT_SEARCH + "')")
+    @Transactional(readOnly = true)
+    public Pair<Integer, List<AuditEntryTO>> search(
+        final String key,
+        final int page,
+        final int size,
+        final List<OrderByClause> orderByClauses) {
+
+        Integer count = auditDAO.count(key);
+        List<AuditEntry> matching = auditDAO.findByEntityKey(key, page, size, orderByClauses);
+        List<AuditEntryTO> results = matching.stream().
+            map(audit -> binder.returnAuditTO(binder.getAuditTO(audit))).
+            collect(Collectors.toList());
+        return Pair.of(count, results);
+    }
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AuditDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AuditDAO.java
new file mode 100644
index 0000000..19cf75a
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AuditDAO.java
@@ -0,0 +1,30 @@
+/*
+ * 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.core.persistence.api.dao;
+
+import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
+import org.apache.syncope.core.persistence.api.entity.AuditEntry;
+
+import java.util.List;
+
+public interface AuditDAO<T extends AuditEntry> {
+    List<AuditEntry> findByEntityKey(String key, int page, int size, List<OrderByClause> orderByClauses);
+
+    Integer count(String key);
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/AuditEntry.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/AuditEntry.java
new file mode 100644
index 0000000..7bcb134
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/AuditEntry.java
@@ -0,0 +1,43 @@
+/*
+ * 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.core.persistence.api.entity;
+
+import org.apache.syncope.common.lib.types.AuditLoggerName;
+
+import java.io.Serializable;
+import java.util.Date;
+
+public interface AuditEntry extends Serializable {
+
+    String getWho();
+
+    AuditLoggerName getLogger();
+
+    Object getBefore();
+
+    Object getOutput();
+
+    Object[] getInput();
+
+    String getThrowable();
+
+    Date getDate();
+
+    String getKey();
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditDAO.java
new file mode 100644
index 0000000..919217a
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditDAO.java
@@ -0,0 +1,108 @@
+/*
+ * 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.core.persistence.jpa.dao;
+
+import org.apache.syncope.core.persistence.api.DomainsHolder;
+import org.apache.syncope.core.persistence.api.dao.AuditDAO;
+import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
+import org.apache.syncope.core.persistence.api.entity.AuditEntry;
+import org.apache.syncope.core.provisioning.api.AuditEntryImpl;
+import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.sql.DataSource;
+
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Transactional(rollbackFor = Throwable.class)
+@Repository
+public class JPAAuditDAO extends AbstractDAO implements AuditDAO<AuditEntry> {
+    private static final Logger LOG = LoggerFactory.getLogger(JPAAuditDAO.class);
+
+    private static final String TABLE_NAME = "SYNCOPEAUDIT";
+
+    @Autowired
+    private DomainsHolder domainsHolder;
+
+    private static String buildWhereClauseForEntityKey(final String key) {
+        return " WHERE MESSAGE LIKE '%" + key + "%' ";
+    }
+
+    @Override
+    public List<AuditEntry> findByEntityKey(final String key, final int page,
+                                            final int itemsPerPage,
+                                            final List<OrderByClause> orderByClauses) {
+        try {
+            String queryString = "SELECT * FROM " + TABLE_NAME + buildWhereClauseForEntityKey(key);
+            if (!orderByClauses.isEmpty()) {
+                queryString += " ORDER BY " + orderByClauses.
+                    stream().
+                    map(orderBy -> orderBy.getField() + ' ' + orderBy.getDirection().name()).
+                    collect(Collectors.joining(","));
+            }
+            JdbcTemplate template = getJdbcTemplate();
+            template.setMaxRows(itemsPerPage);
+            template.setFetchSize(itemsPerPage * (page <= 0 ? 0 : page - 1));
+            return template.query(queryString, (resultSet, i) -> {
+                AuditEntryImpl entry = POJOHelper.deserialize(resultSet.getString("MESSAGE"), AuditEntryImpl.class);
+                String throwable = resultSet.getString("THROWABLE");
+                entry.setThrowable(throwable);
+                Timestamp date = resultSet.getTimestamp("EVENT_DATE");
+                entry.setDate(new Date(date.getTime()));
+                entry.setKey(key);
+                return entry;
+            });
+        } catch (Exception e) {
+            LOG.error("Unable to execute search query to find entity " + key, e);
+        }
+        return Collections.emptyList();
+    }
+
+    @Override
+    public Integer count(final String key) {
+        try {
+            String queryString = "SELECT COUNT(0) FROM " + TABLE_NAME + buildWhereClauseForEntityKey(key);
+            return Objects.requireNonNull(getJdbcTemplate().queryForObject(queryString, Integer.class));
+        } catch (Exception e) {
+            LOG.error("Unable to execute count query for entity " + key, e);
+        }
+        return 0;
+    }
+
+    private JdbcTemplate getJdbcTemplate() {
+        String domain = AuthContextUtils.getDomain();
+        DataSource datasource = domainsHolder.getDomains().get(domain);
+        if (datasource == null) {
+            throw new IllegalArgumentException("Could not get to DataSource for domain " + domain);
+        }
+        return new JdbcTemplate(datasource);
+    }
+}
diff --git a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
index 05326aa..078f57e 100644
--- a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
+++ b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
@@ -2561,4 +2561,60 @@ $$ }&#10;
   <SyncopeLogger logName="syncope.audit.[LOGIC]:[SyncopeLogic]:[]:[isSelfRegAllowed]:[SUCCESS]" logLevel="DEBUG" logType="AUDIT"/>
   
   <SecurityQuestion id="887028ea-66fc-41e7-b397-620d7ea6dfbb" content="What's your mother's maiden name?"/>
+
+  <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"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[confirmPasswordReset]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[create]:[FAILURE]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[create]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[delete]:[FAILURE]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[delete]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[deprovision]:[FAILURE]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[deprovision]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[link]:[FAILURE]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[link]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[mustChangePassword]:[FAILURE]" LOGLEVEL="DEBUG"/>
+  <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"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfDelete]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfRead]:[FAILURE]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfRead]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfStatus]:[FAILURE]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfStatus]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfUpdate]:[FAILURE]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[selfUpdate]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[status]:[FAILURE]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[status]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[unassign]:[FAILURE]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[unassign]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[unlink]:[FAILURE]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[unlink]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[update]:[FAILURE]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[UserLogic]:[]:[update]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[GroupLogic]:[]:[assign]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[GroupLogic]:[]:[create]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[GroupLogic]:[]:[delete]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[GroupLogic]:[]:[deprovision]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[GroupLogic]:[]:[link]:[SUCCESS]" LOGLEVEL="DEBUG"/>
+  <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"/>
+  <SYNCOPELOGGER LOGTYPE="AUDIT" LOGNAME="syncope.audit.[LOGIC]:[SyncopeLogic]:[]:[isSelfRegAllowed]:[SUCCESS]" LOGLEVEL="DEBUG"/>
 </dataset>
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/AuditEntry.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/AuditEntryImpl.java
similarity index 50%
rename from core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/AuditEntry.java
rename to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/AuditEntryImpl.java
index b117beb..a6fde1b 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/AuditEntry.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/AuditEntryImpl.java
@@ -16,18 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java;
+package org.apache.syncope.core.provisioning.api;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import java.io.Serializable;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.SerializationUtils;
 import org.apache.syncope.common.lib.patch.UserPatch;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.AuditLoggerName;
+import org.apache.syncope.core.persistence.api.entity.AuditEntry;
 
-public class AuditEntry implements Serializable {
+import java.util.Date;
+
+public class AuditEntryImpl implements AuditEntry {
 
     private static final long serialVersionUID = -2299082316063743582L;
 
@@ -43,13 +45,19 @@ public class AuditEntry implements Serializable {
 
     private final Object[] input;
 
+    private String throwable;
+
+    private Date date;
+
+    private String key;
+    
     @JsonCreator
-    public AuditEntry(
-            @JsonProperty("who") final String who,
-            @JsonProperty("logger") final AuditLoggerName logger,
-            @JsonProperty("before") final Object before,
-            @JsonProperty("output") final Object output,
-            @JsonProperty("input") final Object[] input) {
+    public AuditEntryImpl(
+        @JsonProperty("who") final String who,
+        @JsonProperty("logger") final AuditLoggerName logger,
+        @JsonProperty("before") final Object before,
+        @JsonProperty("output") final Object output,
+        @JsonProperty("input") final Object[] input) {
 
         super();
 
@@ -65,7 +73,7 @@ public class AuditEntry implements Serializable {
         }
     }
 
-    private Object maskSensitive(final Object object) {
+    private static Object maskSensitive(final Object object) {
         Object masked;
 
         if (object instanceof UserTO) {
@@ -86,23 +94,128 @@ public class AuditEntry implements Serializable {
         return masked;
     }
 
+    @Override
     public String getWho() {
         return who;
     }
 
+    @Override
     public AuditLoggerName getLogger() {
         return logger;
     }
 
+    @Override
     public Object getBefore() {
         return before;
     }
 
+    @Override
     public Object getOutput() {
         return output;
     }
 
+    @Override
     public Object[] getInput() {
         return input;
     }
+
+    @Override
+    public String getThrowable() {
+        return throwable;
+    }
+
+    public void setThrowable(final String throwable) {
+        this.throwable = throwable;
+    }
+
+    @Override
+    public Date getDate() {
+        return date;
+    }
+
+    public void setDate(final Date date) {
+        this.date = date;
+    }
+
+    @Override
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(final String key) {
+        this.key = key;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static final class Builder {
+        private String who;
+
+        private AuditLoggerName logger;
+
+        private Object before;
+
+        private Object output;
+
+        private Object[] input;
+
+        private String throwable;
+
+        private Date date;
+
+        private String key;
+
+        private Builder() {
+        }
+
+        public Builder date(final Date date) {
+            this.date = date;
+            return this;
+        }
+
+        public Builder throwable(final String throwable) {
+            this.throwable = throwable;
+            return this;
+        }
+
+        public Builder key(final String key) {
+            this.key = key;
+            return this;
+        }
+
+        public Builder who(final String who) {
+            this.who = who;
+            return this;
+        }
+
+        public Builder logger(final AuditLoggerName logger) {
+            this.logger = logger;
+            return this;
+        }
+
+        public Builder before(final Object before) {
+            this.before = before;
+            return this;
+        }
+
+        public Builder output(final Object output) {
+            this.output = output;
+            return this;
+        }
+
+        public Builder input(final Object[] input) {
+            this.input = input;
+            return this;
+        }
+
+        public AuditEntryImpl build() {
+            AuditEntryImpl entry = new AuditEntryImpl(who, logger, before, output, input);
+            entry.setDate(date);
+            entry.setThrowable(throwable);
+            entry.setKey(key);
+            return entry;
+        }
+    }
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/AuditDataBinder.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/AuditDataBinder.java
new file mode 100644
index 0000000..81e3241
--- /dev/null
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/AuditDataBinder.java
@@ -0,0 +1,28 @@
+/*
+ * 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.core.provisioning.api.data;
+
+import org.apache.syncope.common.lib.to.AuditEntryTO;
+import org.apache.syncope.core.persistence.api.entity.AuditEntry;
+
+public interface AuditDataBinder {
+    AuditEntryTO getAuditTO(AuditEntry application);
+
+    AuditEntryTO returnAuditTO(AuditEntryTO user);
+}
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 3d44e35..0f9e169 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
@@ -18,6 +18,8 @@
  */
 package org.apache.syncope.core.provisioning.java;
 
+import org.apache.syncope.core.persistence.api.entity.AuditEntry;
+import org.apache.syncope.core.provisioning.api.AuditEntryImpl;
 import org.apache.syncope.core.provisioning.api.AuditManager;
 import org.apache.syncope.common.lib.types.AuditElements;
 import org.apache.syncope.common.lib.types.AuditElements.Result;
@@ -47,12 +49,10 @@ public class DefaultAuditManager implements AuditManager {
             final String subcategory,
             final String event) {
 
-        AuditEntry auditEntry = new AuditEntry(
-                who,
-                new AuditLoggerName(type, category, subcategory, event, Result.SUCCESS),
-                null,
-                null,
-                null);
+        AuditEntry auditEntry = AuditEntryImpl.builder()
+            .who(who)
+            .logger(new AuditLoggerName(type, category, subcategory, event, Result.SUCCESS))
+            .build();
         org.apache.syncope.core.persistence.api.entity.Logger syncopeLogger =
                 loggerDAO.find(auditEntry.getLogger().toLoggerName());
         boolean auditRequested = syncopeLogger != null && syncopeLogger.getLevel() == LoggerLevel.DEBUG;
@@ -61,12 +61,10 @@ public class DefaultAuditManager implements AuditManager {
             return true;
         }
 
-        auditEntry = new AuditEntry(
-                who,
-                new AuditLoggerName(type, category, subcategory, event, Result.FAILURE),
-                null,
-                null,
-                null);
+        auditEntry = AuditEntryImpl.builder()
+            .who(who)
+            .logger(new AuditLoggerName(type, category, subcategory, event, Result.FAILURE))
+            .build();
         syncopeLogger = loggerDAO.find(auditEntry.getLogger().toLoggerName());
         auditRequested = syncopeLogger != null && syncopeLogger.getLevel() == LoggerLevel.DEBUG;
 
@@ -106,13 +104,14 @@ public class DefaultAuditManager implements AuditManager {
             throwable = (Throwable) output;
         }
 
-        AuditEntry auditEntry = new AuditEntry(
-                who,
-                new AuditLoggerName(type, category, subcategory, event, condition),
-                before,
-                throwable == null ? output : throwable.getMessage(),
-                input);
-
+        AuditEntry auditEntry = AuditEntryImpl.builder()
+            .who(who)
+            .logger(new AuditLoggerName(type, category, subcategory, event, condition))
+            .before(before)
+            .output(throwable == null ? output : throwable.getMessage())
+            .input(input)
+            .build();
+        
         org.apache.syncope.core.persistence.api.entity.Logger syncopeLogger =
                 loggerDAO.find(auditEntry.getLogger().toLoggerName());
         if (syncopeLogger != null && syncopeLogger.getLevel() == LoggerLevel.DEBUG) {
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
new file mode 100644
index 0000000..c268270
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuditDataBinderImpl.java
@@ -0,0 +1,57 @@
+package org.apache.syncope.core.provisioning.java.data;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import org.apache.syncope.common.lib.to.AuditEntryTO;
+import org.apache.syncope.core.persistence.api.entity.AuditEntry;
+import org.apache.syncope.core.provisioning.api.data.AuditDataBinder;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+@Component
+public class AuditDataBinderImpl implements AuditDataBinder {
+
+    @Override
+    public AuditEntryTO getAuditTO(final AuditEntry auditEntry) {
+        AuditEntryTO auditTO = new AuditEntryTO();
+        auditTO.setKey(auditEntry.getKey());
+        auditTO.setWho(auditEntry.getWho());
+        auditTO.setDate(auditEntry.getDate());
+        auditTO.setThrowable(auditEntry.getThrowable());
+        auditTO.setLoggerName(auditEntry.getLogger().toLoggerName());
+
+
+        auditTO.setSubCategory(auditEntry.getLogger().getSubcategory());
+        auditTO.setEvent(auditEntry.getLogger().getEvent());
+
+        if (auditEntry.getLogger().getResult() != null) {
+            auditTO.setResult(auditEntry.getLogger().getResult().name());
+        }
+
+        if (auditEntry.getBefore() != null) {
+            String before = ToStringBuilder.reflectionToString(
+                auditEntry.getBefore(), ToStringStyle.JSON_STYLE);
+            auditTO.setBefore(before);
+        }
+
+        if (auditEntry.getInput() != null) {
+            auditTO.getInputs().addAll(Arrays.stream(auditEntry.getInput())
+                .map(input -> ToStringBuilder.reflectionToString(input, ToStringStyle.JSON_STYLE))
+                .collect(Collectors.toList()));
+        }
+
+        if (auditEntry.getOutput() != null) {
+            auditTO.setOutput(ToStringBuilder.reflectionToString(
+                auditEntry.getOutput(), ToStringStyle.JSON_STYLE));
+        }
+        
+        return auditTO;
+    }
+
+    @Override
+    public AuditEntryTO returnAuditTO(final AuditEntryTO auditEntryTO) {
+        return auditEntryTO;
+    }
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/AuditReportlet.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/AuditReportlet.java
index 6ba9a53..c7af73d 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/AuditReportlet.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/AuditReportlet.java
@@ -27,7 +27,8 @@ import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 import org.apache.syncope.common.lib.report.AuditReportletConf;
 import org.apache.syncope.common.lib.report.ReportletConf;
-import org.apache.syncope.core.provisioning.java.AuditEntry;
+import org.apache.syncope.core.persistence.api.entity.AuditEntry;
+import org.apache.syncope.core.provisioning.api.AuditEntryImpl;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.core.persistence.api.DomainsHolder;
@@ -59,7 +60,7 @@ public class AuditReportlet extends AbstractReportlet {
         handler.startElement("", "", "events", null);
         AttributesImpl atts = new AttributesImpl();
         for (Map<String, Object> row : rows) {
-            AuditEntry auditEntry = POJOHelper.deserialize(row.get("MESSAGE").toString(), AuditEntry.class);
+            AuditEntry auditEntry = POJOHelper.deserialize(row.get("MESSAGE").toString(), AuditEntryImpl.class);
 
             atts.clear();
             if (StringUtils.isNotBlank(auditEntry.getWho())) {
diff --git a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractAnyService.java b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractAnyService.java
index 2d8c3b4..84f0213 100644
--- a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractAnyService.java
+++ b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractAnyService.java
@@ -56,8 +56,8 @@ import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.core.spring.security.SecureRandomUtils;
 
 public abstract class AbstractAnyService<TO extends AnyTO, P extends AnyPatch>
-        extends AbstractServiceImpl
-        implements AnyService<TO> {
+    extends AbstractServiceImpl
+    implements AnyService<TO> {
 
     protected abstract AnyDAO<?> getAnyDAO();
 
@@ -122,20 +122,20 @@ public abstract class AbstractAnyService<TO extends AnyTO, P extends AnyPatch>
 
         // if an assignable query is provided in the FIQL string, start anyway from root realm
         boolean isAssignableCond = StringUtils.isBlank(anyQuery.getFiql())
-                ? false
-                : -1 != anyQuery.getFiql().indexOf(SpecialAttr.ASSIGNABLE.toString());
+            ? false
+            : -1 != anyQuery.getFiql().indexOf(SpecialAttr.ASSIGNABLE.toString());
 
         SearchCond searchCond = StringUtils.isBlank(anyQuery.getFiql())
-                ? null
-                : getSearchCond(anyQuery.getFiql(), realm);
+            ? null
+            : getSearchCond(anyQuery.getFiql(), realm);
 
         Pair<Integer, List<TO>> result = getAnyLogic().search(
-                searchCond,
-                anyQuery.getPage(),
-                anyQuery.getSize(),
-                getOrderByClauses(anyQuery.getOrderBy()),
-                isAssignableCond ? SyncopeConstants.ROOT_REALM : realm,
-                anyQuery.getDetails());
+            searchCond,
+            anyQuery.getPage(),
+            anyQuery.getSize(),
+            getOrderByClauses(anyQuery.getOrderBy()),
+            isAssignableCond ? SyncopeConstants.ROOT_REALM : realm,
+            anyQuery.getDetails());
 
         return buildPagedResult(result.getRight(), anyQuery.getPage(), anyQuery.getSize(), result.getLeft());
     }
@@ -159,7 +159,7 @@ public abstract class AbstractAnyService<TO extends AnyTO, P extends AnyPatch>
     }
 
     private void addUpdateOrReplaceAttr(
-            final String key, final SchemaType schemaType, final AttrTO attrTO, final PatchOperation operation) {
+        final String key, final SchemaType schemaType, final AttrTO attrTO, final PatchOperation operation) {
 
         if (attrTO.getSchema() == null) {
             throw new NotFoundException("Must specify schema");
@@ -193,10 +193,10 @@ public abstract class AbstractAnyService<TO extends AnyTO, P extends AnyPatch>
     @Override
     public void delete(final String key, final SchemaType schemaType, final String schema) {
         addUpdateOrReplaceAttr(
-                getActualKey(getAnyDAO(), key),
-                schemaType,
-                new AttrTO.Builder().schema(schema).build(),
-                PatchOperation.DELETE);
+            getActualKey(getAnyDAO(), key),
+            schemaType,
+            new AttrTO.Builder().schema(schema).build(),
+            PatchOperation.DELETE);
     }
 
     @Override
@@ -242,13 +242,13 @@ public abstract class AbstractAnyService<TO extends AnyTO, P extends AnyPatch>
                 item.getHeaders().put(RESTHeaders.RESOURCE_KEY, Arrays.asList(resource));
 
                 item.setStatus(updated.getEntity().getResources().contains(resource)
-                        ? Response.Status.BAD_REQUEST.getStatusCode()
-                        : Response.Status.OK.getStatusCode());
+                    ? Response.Status.BAD_REQUEST.getStatusCode()
+                    : Response.Status.OK.getStatusCode());
 
                 if (getPreference() == Preference.RETURN_NO_CONTENT) {
                     item.getHeaders().put(
-                            RESTHeaders.PREFERENCE_APPLIED,
-                            Arrays.asList(Preference.RETURN_NO_CONTENT.toString()));
+                        RESTHeaders.PREFERENCE_APPLIED,
+                        Arrays.asList(Preference.RETURN_NO_CONTENT.toString()));
                 } else {
                     item.setContent(POJOHelper.serialize(updated.getEntity()));
                 }
@@ -257,34 +257,34 @@ public abstract class AbstractAnyService<TO extends AnyTO, P extends AnyPatch>
             }).collect(Collectors.toList());
         } else {
             batchResponseItems = updated.getPropagationStatuses().stream().
-                    map(status -> {
-                        BatchResponseItem item = new BatchResponseItem();
+                map(status -> {
+                    BatchResponseItem item = new BatchResponseItem();
 
-                        item.getHeaders().put(RESTHeaders.RESOURCE_KEY, Arrays.asList(status.getResource()));
+                    item.getHeaders().put(RESTHeaders.RESOURCE_KEY, Arrays.asList(status.getResource()));
 
-                        item.setStatus(status.getStatus().getHttpStatus());
+                    item.setStatus(status.getStatus().getHttpStatus());
 
-                        if (status.getFailureReason() != null) {
-                            item.getHeaders().put(RESTHeaders.ERROR_INFO, Arrays.asList(status.getFailureReason()));
-                        }
+                    if (status.getFailureReason() != null) {
+                        item.getHeaders().put(RESTHeaders.ERROR_INFO, Arrays.asList(status.getFailureReason()));
+                    }
 
-                        if (getPreference() == Preference.RETURN_NO_CONTENT) {
-                            item.getHeaders().put(
-                                    RESTHeaders.PREFERENCE_APPLIED,
-                                    Arrays.asList(Preference.RETURN_NO_CONTENT.toString()));
-                        } else {
-                            item.setContent(POJOHelper.serialize(updated.getEntity()));
-                        }
+                    if (getPreference() == Preference.RETURN_NO_CONTENT) {
+                        item.getHeaders().put(
+                            RESTHeaders.PREFERENCE_APPLIED,
+                            Arrays.asList(Preference.RETURN_NO_CONTENT.toString()));
+                    } else {
+                        item.setContent(POJOHelper.serialize(updated.getEntity()));
+                    }
 
-                        return item;
-                    }).collect(Collectors.toList());
+                    return item;
+                }).collect(Collectors.toList());
         }
 
         String boundary = "deassociate_" + SecureRandomUtils.generateRandomUUID().toString();
         return Response.ok(BatchPayloadGenerator.generate(
-                batchResponseItems, SyncopeConstants.DOUBLE_DASH + boundary)).
-                type(RESTHeaders.multipartMixedWith(boundary)).
-                build();
+            batchResponseItems, SyncopeConstants.DOUBLE_DASH + boundary)).
+            type(RESTHeaders.multipartMixedWith(boundary)).
+            build();
     }
 
     @Override
@@ -297,26 +297,26 @@ public abstract class AbstractAnyService<TO extends AnyTO, P extends AnyPatch>
             case LINK:
                 updated = new ProvisioningResult<>();
                 updated.setEntity(getAnyLogic().link(
-                        patch.getKey(),
-                        patch.getResources()));
+                    patch.getKey(),
+                    patch.getResources()));
                 break;
 
             case ASSIGN:
                 updated = getAnyLogic().assign(
-                        patch.getKey(),
-                        patch.getResources(),
-                        patch.getValue() != null,
-                        patch.getValue(),
-                        isNullPriorityAsync());
+                    patch.getKey(),
+                    patch.getResources(),
+                    patch.getValue() != null,
+                    patch.getValue(),
+                    isNullPriorityAsync());
                 break;
 
             case PROVISION:
                 updated = getAnyLogic().provision(
-                        patch.getKey(),
-                        patch.getResources(),
-                        patch.getValue() != null,
-                        patch.getValue(),
-                        isNullPriorityAsync());
+                    patch.getKey(),
+                    patch.getResources(),
+                    patch.getValue() != null,
+                    patch.getValue(),
+                    isNullPriorityAsync());
                 break;
 
             default:
@@ -331,13 +331,13 @@ public abstract class AbstractAnyService<TO extends AnyTO, P extends AnyPatch>
                 item.getHeaders().put(RESTHeaders.RESOURCE_KEY, Arrays.asList(resource));
 
                 item.setStatus(updated.getEntity().getResources().contains(resource)
-                        ? Response.Status.OK.getStatusCode()
-                        : Response.Status.BAD_REQUEST.getStatusCode());
+                    ? Response.Status.OK.getStatusCode()
+                    : Response.Status.BAD_REQUEST.getStatusCode());
 
                 if (getPreference() == Preference.RETURN_NO_CONTENT) {
                     item.getHeaders().put(
-                            RESTHeaders.PREFERENCE_APPLIED,
-                            Arrays.asList(Preference.RETURN_NO_CONTENT.toString()));
+                        RESTHeaders.PREFERENCE_APPLIED,
+                        Arrays.asList(Preference.RETURN_NO_CONTENT.toString()));
                 } else {
                     item.setContent(POJOHelper.serialize(updated.getEntity()));
                 }
@@ -346,33 +346,33 @@ public abstract class AbstractAnyService<TO extends AnyTO, P extends AnyPatch>
             }).collect(Collectors.toList());
         } else {
             batchResponseItems = updated.getPropagationStatuses().stream().
-                    map(status -> {
-                        BatchResponseItem item = new BatchResponseItem();
+                map(status -> {
+                    BatchResponseItem item = new BatchResponseItem();
 
-                        item.getHeaders().put(RESTHeaders.RESOURCE_KEY, Arrays.asList(status.getResource()));
+                    item.getHeaders().put(RESTHeaders.RESOURCE_KEY, Arrays.asList(status.getResource()));
 
-                        item.setStatus(status.getStatus().getHttpStatus());
+                    item.setStatus(status.getStatus().getHttpStatus());
 
-                        if (status.getFailureReason() != null) {
-                            item.getHeaders().put(RESTHeaders.ERROR_INFO, Arrays.asList(status.getFailureReason()));
-                        }
+                    if (status.getFailureReason() != null) {
+                        item.getHeaders().put(RESTHeaders.ERROR_INFO, Arrays.asList(status.getFailureReason()));
+                    }
 
-                        if (getPreference() == Preference.RETURN_NO_CONTENT) {
-                            item.getHeaders().put(
-                                    RESTHeaders.PREFERENCE_APPLIED,
-                                    Arrays.asList(Preference.RETURN_NO_CONTENT.toString()));
-                        } else {
-                            item.setContent(POJOHelper.serialize(updated.getEntity()));
-                        }
+                    if (getPreference() == Preference.RETURN_NO_CONTENT) {
+                        item.getHeaders().put(
+                            RESTHeaders.PREFERENCE_APPLIED,
+                            Arrays.asList(Preference.RETURN_NO_CONTENT.toString()));
+                    } else {
+                        item.setContent(POJOHelper.serialize(updated.getEntity()));
+                    }
 
-                        return item;
-                    }).collect(Collectors.toList());
+                    return item;
+                }).collect(Collectors.toList());
         }
 
         String boundary = "associate_" + SecureRandomUtils.generateRandomUUID().toString();
         return Response.ok(BatchPayloadGenerator.generate(
-                batchResponseItems, SyncopeConstants.DOUBLE_DASH + boundary)).
-                type(RESTHeaders.multipartMixedWith(boundary)).
-                build();
+            batchResponseItems, SyncopeConstants.DOUBLE_DASH + boundary)).
+            type(RESTHeaders.multipartMixedWith(boundary)).
+            build();
     }
 }
diff --git a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AuditServiceImpl.java b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AuditServiceImpl.java
new file mode 100644
index 0000000..e13827a
--- /dev/null
+++ b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AuditServiceImpl.java
@@ -0,0 +1,48 @@
+/*
+ * 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.core.rest.cxf.service;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.to.AuditEntryTO;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.rest.api.beans.AuditQuery;
+import org.apache.syncope.common.rest.api.service.AuditService;
+import org.apache.syncope.core.logic.AuditLogic;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class AuditServiceImpl extends AbstractServiceImpl implements AuditService {
+    @Autowired
+    private AuditLogic logic;
+
+    @Override
+    public PagedResult<AuditEntryTO> search(final AuditQuery auditQuery) {
+        Pair<Integer, List<AuditEntryTO>> result = logic.search(
+            auditQuery.getKey(),
+            auditQuery.getPage(),
+            auditQuery.getSize(),
+            getOrderByClauses(auditQuery.getOrderBy())
+            );
+        return buildPagedResult(result.getRight(), auditQuery.getPage(), auditQuery.getSize(), result.getLeft());
+    }
+
+}
diff --git a/fit/core-reference/pom.xml b/fit/core-reference/pom.xml
index d980a12..1249f21 100644
--- a/fit/core-reference/pom.xml
+++ b/fit/core-reference/pom.xml
@@ -338,7 +338,7 @@ under the License.
             <deployable>
               <location>${project.build.directory}/${project.build.finalName}</location>
               <pingURL>http://localhost:${cargo.servlet.port}/syncope/cacheStats.jsp</pingURL>
-              <pingTimeout>60000</pingTimeout>
+              <pingTimeout>180000</pingTimeout>
               <properties>
                 <context>syncope</context>
               </properties>
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
index be0dcee..b932689 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
@@ -77,6 +77,7 @@ import org.apache.syncope.common.rest.api.service.AnyObjectService;
 import org.apache.syncope.common.rest.api.service.AnyTypeClassService;
 import org.apache.syncope.common.rest.api.service.AnyTypeService;
 import org.apache.syncope.common.rest.api.service.ApplicationService;
+import org.apache.syncope.common.rest.api.service.AuditService;
 import org.apache.syncope.common.rest.api.service.CamelRouteService;
 import org.apache.syncope.common.rest.api.service.ConfigurationService;
 import org.apache.syncope.common.rest.api.service.ConnectorHistoryService;
@@ -281,6 +282,8 @@ public abstract class AbstractITCase {
 
     protected static SCIMConfService scimConfService;
 
+    protected static AuditService auditService;
+
     @BeforeAll
     public static void securitySetup() {
         try (InputStream propStream = Encryptor.class.getResourceAsStream("/security.properties")) {
@@ -353,6 +356,7 @@ public abstract class AbstractITCase {
         oidcClientService = adminClient.getService(OIDCClientService.class);
         oidcProviderService = adminClient.getService(OIDCProviderService.class);
         scimConfService = adminClient.getService(SCIMConfService.class);
+        auditService = adminClient.getService(AuditService.class);
     }
 
     protected static String getUUIDString() {
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
new file mode 100644
index 0000000..dfcbebc
--- /dev/null
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuditITCase.java
@@ -0,0 +1,108 @@
+/*
+ * 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.fit.core;
+
+import org.apache.syncope.common.lib.SyncopeConstants;
+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.ProvisioningResult;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.rest.api.beans.AuditQuery;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class AuditITCase extends AbstractITCase {
+    private static final String USER_KEY = getUUIDString();
+    private static final String GROUP_KEY = getUUIDString();
+
+    private static GroupTO getSampleGroupTO(final String name) {
+        GroupTO groupTO = new GroupTO();
+        groupTO.setRealm(SyncopeConstants.ROOT_REALM);
+        groupTO.setName(name + getUUIDString());
+        groupTO.setKey(GROUP_KEY);
+        return groupTO;
+    }
+
+    private static UserTO getSampleUserTO(final String email) {
+        UserTO userTO = new UserTO();
+        userTO.setRealm(SyncopeConstants.ROOT_REALM);
+        userTO.setPassword("password123");
+        userTO.setUsername(email);
+        userTO.setKey(USER_KEY);
+        userTO.getPlainAttrs().add(attrTO("fullname", "Apache Syncope"));
+        userTO.getPlainAttrs().add(attrTO("firstname", "Apache"));
+        userTO.getPlainAttrs().add(attrTO("surname", "Syncope"));
+        userTO.getPlainAttrs().add(attrTO("userId", email));
+        userTO.getPlainAttrs().add(attrTO("email", email));
+        return userTO;
+    }
+
+    @Test
+    public void findByUser() {
+        UserTO userTO = getSampleUserTO("example@syncope.org");
+        userTO.getResources().add(RESOURCE_NAME_TESTDB);
+        ProvisioningResult<UserTO> result = createUser(userTO);
+        assertNotNull(result);
+        userTO = result.getEntity();
+        userService.read(userTO.getKey());
+
+        PagedResult<AuditEntryTO> auditResult = auditService.search(
+            new AuditQuery.Builder()
+                .key(USER_KEY)
+                .orderBy("event_date desc")
+                .page(1)
+                .size(1)
+                .build());
+        assertNotNull(auditResult);
+        List<AuditEntryTO> results = auditResult.getResult();
+        assertFalse(results.isEmpty());
+        assertEquals(1, results.size());
+        assertTrue(results.stream().allMatch(entry -> entry.getKey().equalsIgnoreCase(USER_KEY)));
+        userService.delete(userTO.getKey());
+    }
+
+    @Test
+    public void findByGroup() {
+        GroupTO groupTO = getSampleGroupTO("AuditGroup");
+        ProvisioningResult<GroupTO> groupResult = createGroup(groupTO);
+        assertNotNull(groupResult);
+        groupTO = groupResult.getEntity();
+        groupService.read(groupTO.getKey());
+
+        PagedResult<AuditEntryTO> result = auditService.search(
+            new AuditQuery.Builder()
+                .key(GROUP_KEY)
+                .orderBy("event_date asc")
+                .page(1)
+                .size(1)
+                .build());
+        assertNotNull(result);
+        List<AuditEntryTO> results = result.getResult();
+        assertFalse(results.isEmpty());
+        assertEquals(1, results.size());
+        assertTrue(results.stream().allMatch(entry -> entry.getKey().equalsIgnoreCase(GROUP_KEY)));
+        groupService.delete(groupTO.getKey());
+    }
+}