diff --git a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/audit-common-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/audit-common-SqlMap.xml index dce92afeb9..5084416677 100644 --- a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/audit-common-SqlMap.xml +++ b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/audit-common-SqlMap.xml @@ -135,14 +135,20 @@ delete from alf_audit_entry - where - audit_app_id = #auditApplicationId# - - = #auditFromTime#]]> - - - - + + + audit_app_id = #auditApplicationId# + + + = #auditFromTime#]]> + + + + + + id in #auditEntryIds[]# + + diff --git a/source/java/org/alfresco/repo/audit/AuditComponent.java b/source/java/org/alfresco/repo/audit/AuditComponent.java index 3c742ac2d9..7f5deaf88b 100644 --- a/source/java/org/alfresco/repo/audit/AuditComponent.java +++ b/source/java/org/alfresco/repo/audit/AuditComponent.java @@ -19,6 +19,7 @@ package org.alfresco.repo.audit; import java.io.Serializable; +import java.util.List; import java.util.Map; import org.alfresco.repo.audit.model.AuditApplication; @@ -89,6 +90,14 @@ public interface AuditComponent */ int deleteAuditEntries(String applicationName, Long fromTime, Long toTime); + /** + * Delete a discrete list of audit entries based on ID + * + * @param auditEntryIds the audit entry IDs to delete + * @return Returns the number of entries deleted + */ + int deleteAuditEntries(List auditEntryIds); + /** * Check if an audit path is enabled. The path will be disabled if it or any higher * path has been explicitly disabled. Any disabled path will not be processed when diff --git a/source/java/org/alfresco/repo/audit/AuditComponentImpl.java b/source/java/org/alfresco/repo/audit/AuditComponentImpl.java index 9a230125f0..22ded77f3a 100644 --- a/source/java/org/alfresco/repo/audit/AuditComponentImpl.java +++ b/source/java/org/alfresco/repo/audit/AuditComponentImpl.java @@ -146,6 +146,21 @@ public class AuditComponentImpl implements AuditComponent return deleted; } + /** + * {@inheritDoc} + * @since 3.2 + */ + @Override + public int deleteAuditEntries(List auditEntryIds) + { + // Shortcut, if necessary + if (auditEntryIds.size() == 0) + { + return 0; + } + return auditDAO.deleteAuditEntries(auditEntryIds); + } + /** * @param application the audit application object * @return Returns a copy of the set of disabled paths associated with the application diff --git a/source/java/org/alfresco/repo/audit/AuditComponentTest.java b/source/java/org/alfresco/repo/audit/AuditComponentTest.java index 18239918f7..4c25407657 100644 --- a/source/java/org/alfresco/repo/audit/AuditComponentTest.java +++ b/source/java/org/alfresco/repo/audit/AuditComponentTest.java @@ -523,7 +523,7 @@ public class AuditComponentTest extends TestCase auditModelRegistry.registerModel(testModelUrl); auditModelRegistry.loadAuditModels(); - final List> results = new ArrayList>(); + final List results = new ArrayList(5); final StringBuilder sb = new StringBuilder(); AuditQueryCallback auditQueryCallback = new AuditQueryCallback() { @@ -539,7 +539,7 @@ public class AuditComponentTest extends TestCase long time, Map values) { - results.add(values); + results.add(entryId); if (logger.isDebugEnabled()) { logger.debug( @@ -615,11 +615,28 @@ public class AuditComponentTest extends TestCase { // Expected } + try + { + authenticationService.authenticate("banana", "****".toCharArray()); + fail("Invalid authentication attempt should fail"); + } + catch (AuthenticationException e) + { + // Expected + } results.clear(); sb.delete(0, sb.length()); queryAuditLog(auditQueryCallback, params, -1); logger.debug(sb.toString()); - assertFalse("Did not get any audit results after failed login", results.isEmpty()); + assertEquals("Incorrect number of audit entries after failed login", 2, results.size()); + + // Check that we can delete explicit entries + deleteAuditEntries(results); + results.clear(); + sb.delete(0, sb.length()); + queryAuditLog(auditQueryCallback, params, -1); + logger.debug(sb.toString()); + assertEquals("Explicit audit entries were not deleted", 0, results.size()); } public void testAuditQuery_MaxId() throws Exception @@ -677,6 +694,23 @@ public class AuditComponentTest extends TestCase AuthenticationUtil.runAs(work, AuthenticationUtil.getAdminRoleName()); } + /** + * Clearn the audit log as 'admin' + */ + private void deleteAuditEntries(final List auditEntryIds) + { + RunAsWork work = new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + auditService.clearAudit(auditEntryIds); + return null; + } + }; + AuthenticationUtil.runAs(work, AuthenticationUtil.getAdminRoleName()); + } + /** * Clearn the audit log as 'admin' */ diff --git a/source/java/org/alfresco/repo/audit/AuditServiceImpl.java b/source/java/org/alfresco/repo/audit/AuditServiceImpl.java index 09e4128e85..3667cd3087 100644 --- a/source/java/org/alfresco/repo/audit/AuditServiceImpl.java +++ b/source/java/org/alfresco/repo/audit/AuditServiceImpl.java @@ -18,7 +18,7 @@ */ package org.alfresco.repo.audit; -import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -133,6 +133,16 @@ public class AuditServiceImpl implements AuditService return auditComponent.deleteAuditEntries(applicationName, fromTime, toTime); } + /** + * {@inheritDoc} + * @since 3.4 + */ + @Override + public int clearAudit(List auditEntryIds) + { + return auditComponent.deleteAuditEntries(auditEntryIds); + } + /** * {@inheritDoc} * @since 3.3 diff --git a/source/java/org/alfresco/repo/domain/audit/AbstractAuditDAOImpl.java b/source/java/org/alfresco/repo/domain/audit/AbstractAuditDAOImpl.java index 09907ee5bc..3c98762b8b 100644 --- a/source/java/org/alfresco/repo/domain/audit/AbstractAuditDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/audit/AbstractAuditDAOImpl.java @@ -22,11 +22,13 @@ import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.net.URL; +import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.zip.CRC32; import org.alfresco.error.AlfrescoRuntimeException; @@ -41,6 +43,7 @@ import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.dao.ConcurrencyFailureException; import org.springframework.dao.DataIntegrityViolationException; /** @@ -306,7 +309,39 @@ public abstract class AbstractAuditDAOImpl implements AuditDAO return entity.getId(); } + public int deleteAuditEntries(List auditEntryIds) + { + // Ensure that we don't have duplicates + Set ids = new TreeSet(auditEntryIds); + int shouldDelete = ids.size(); + + int deleted = 0; + List batch = new ArrayList(shouldDelete > 512 ? 512 : shouldDelete); + for (Long id : ids) + { + batch.add(id); + if (batch.size() >= 512) + { + deleted += deleteAuditEntriesImpl(batch); + batch.clear(); + } + } + // Process remaining + if (batch.size() > 0) + { + deleted += deleteAuditEntriesImpl(batch); + } + // Check concurrency + if (deleted != shouldDelete) + { + throw new ConcurrencyFailureException( + "Deleted " + deleted + " audit entries out of a set of " + shouldDelete + " unique IDs."); + } + return deleted; + } + protected abstract AuditEntryEntity createAuditEntry(Long applicationId, long time, Long usernameId, Long valuesId); + protected abstract int deleteAuditEntriesImpl(List auditEntryIds); /* * Searches diff --git a/source/java/org/alfresco/repo/domain/audit/AuditDAO.java b/source/java/org/alfresco/repo/domain/audit/AuditDAO.java index afa2c7809c..c676771fd2 100644 --- a/source/java/org/alfresco/repo/domain/audit/AuditDAO.java +++ b/source/java/org/alfresco/repo/domain/audit/AuditDAO.java @@ -20,6 +20,7 @@ package org.alfresco.repo.domain.audit; import java.io.Serializable; import java.net.URL; +import java.util.List; import java.util.Map; import java.util.Set; @@ -166,6 +167,17 @@ public interface AuditDAO */ int deleteAuditEntries(Long applicationId, Long from, Long to); + /** + * Delete a discrete list of audit entries. Duplicate entries are collapsed + * and the number of entries deleted will match the count of unique IDs in + * the list; otherwise a concurrency condition has occured and an exception + * will be generated. + * + * @param auditEntryIds the IDs of all audit entries to delete + * @return Returns the number of entries deleted + */ + int deleteAuditEntries(List auditEntryIds); + /** * Create a new audit entry with the given map of values. * diff --git a/source/java/org/alfresco/repo/domain/audit/AuditDeleteParameters.java b/source/java/org/alfresco/repo/domain/audit/AuditDeleteParameters.java index eba767332d..208c7c103f 100644 --- a/source/java/org/alfresco/repo/domain/audit/AuditDeleteParameters.java +++ b/source/java/org/alfresco/repo/domain/audit/AuditDeleteParameters.java @@ -19,6 +19,7 @@ package org.alfresco.repo.domain.audit; import java.util.Date; +import java.util.List; /** * Deletion parameters for alf_audit_entry table. @@ -31,6 +32,7 @@ public class AuditDeleteParameters private Long auditApplicationId; private Long auditFromTime; private Long auditToTime; + private List auditEntryIds; public AuditDeleteParameters() { @@ -44,6 +46,7 @@ public class AuditDeleteParameters .append("[ auditApplicationId=").append(auditApplicationId) .append(", auditFromTime").append(auditFromTime == null ? null : new Date(auditFromTime)) .append(", auditToTime").append(auditToTime == null ? null : new Date(auditToTime)) + .append(", auditEntryIds").append(auditEntryIds == null ? null : auditEntryIds.size()) .append("]"); return sb.toString(); } @@ -77,4 +80,14 @@ public class AuditDeleteParameters { this.auditToTime = auditToTime; } + + public List getAuditEntryIds() + { + return auditEntryIds; + } + + public void setAuditEntryIds(List auditEntryIds) + { + this.auditEntryIds = auditEntryIds; + } } diff --git a/source/java/org/alfresco/repo/domain/audit/ibatis/AuditDAOImpl.java b/source/java/org/alfresco/repo/domain/audit/ibatis/AuditDAOImpl.java index 0fd0b5c1e8..8d06130051 100644 --- a/source/java/org/alfresco/repo/domain/audit/ibatis/AuditDAOImpl.java +++ b/source/java/org/alfresco/repo/domain/audit/ibatis/AuditDAOImpl.java @@ -170,6 +170,14 @@ public class AuditDAOImpl extends AbstractAuditDAOImpl return template.delete(DELETE_ENTRIES, params); } + @Override + protected int deleteAuditEntriesImpl(List auditEntryIds) + { + AuditDeleteParameters params = new AuditDeleteParameters(); + params.setAuditEntryIds(auditEntryIds); + return template.delete(DELETE_ENTRIES, params); + } + @Override protected AuditEntryEntity createAuditEntry(Long applicationId, long time, Long usernameId, Long valuesId) { diff --git a/source/java/org/alfresco/service/cmr/audit/AuditService.java b/source/java/org/alfresco/service/cmr/audit/AuditService.java index cb4f12b9cd..9d2aec7814 100644 --- a/source/java/org/alfresco/service/cmr/audit/AuditService.java +++ b/source/java/org/alfresco/service/cmr/audit/AuditService.java @@ -19,6 +19,7 @@ package org.alfresco.service.cmr.audit; import java.io.Serializable; +import java.util.List; import java.util.Map; import org.alfresco.service.PublicService; @@ -145,6 +146,19 @@ public interface AuditService */ int clearAudit(String applicationName, Long fromTime, Long toTime); + /** + * Delete a discrete list of audit entries. + *

+ * This method should not be called while processing + * {@link #auditQuery(AuditQueryCallback, AuditQueryParameters, int) query results}. + * + * @param auditEntryIds the IDs of all audit entries to delete + * @return Returns the number of audit entries deleted + * + * @since 3.4 + */ + int clearAudit(List auditEntryIds); + /** * The interface that will be used to give query results to the calling code. *