diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/audit/control.properties b/config/alfresco/templates/webscripts/org/alfresco/repository/audit/control.properties index d81c76d834..b22b46506c 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/audit/control.properties +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/audit/control.properties @@ -2,4 +2,6 @@ audit.err.app.notProvided=Application name not supplied. audit.err.app.notFound=Application not found: {0} audit.err.path.notProvided=No path was supplied after the application name. -audit.err.action.invalid=Parameter 'action' must be either 'enable' or 'disable' \ No newline at end of file +audit.err.action.invalid=Parameter 'action' must be either 'enable' or 'disable' +audit.err.value.classNotFound='valueType' not recognised: {0} +audit.err.value.convertFailed=Unable to convert ''{0}'' to type ''{1}'' \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/audit/query.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/audit/query.get.desc.xml index e15fe0d100..f4788b27f0 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/audit/query.get.desc.xml +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/audit/query.get.desc.xml @@ -1,7 +1,46 @@ Alfresco Audit Service Query - Get audit events - /api/audit/query/{application}?fromId={fromId}&toId={toId}&fromTime={fromTime}&toTime={toTime}&user={user}&forward={forward}&limit={limit}&verbose={verbose}&{k1}={v1}&{k2}={v2} + + + + /api/audit/query/{application}?fromId={fromId}&toId={toId}&fromTime={fromTime}&toTime={toTime}&user={user}&forward={forward}&limit={limit}&verbose={verbose} + /api/audit/query/{application}/{path}?value={value}&valueType={valueType}&fromId={fromId}&toId={toId}&fromTime={fromTime}&toTime={toTime}&user={user}&forward={forward}&limit={limit}&verbose={verbose} admin required @@ -9,15 +48,60 @@ application - Name of the audit application (mandatory parameter) + + + - k1 - First key to query for. If no value is provided, then the present of the key is enough. + path + + + - v1 - First value to query for. If this is no provided, then the presence of the key is enough. + value + + + + + + valueType + + + + + + limit + + + + + + verbose + + + diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/audit/query.get.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/audit/query.get.json.ftl index 1a5e4a7e8d..159610b30b 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/audit/query.get.json.ftl +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/audit/query.get.json.ftl @@ -1,20 +1,21 @@ +<#escape x as jsonUtils.encodeJSONString(x)> { + "count":${count?c}, "entries": [ <#list entries as entry> { - "id":${entry.id}, + "id":${entry.id?c}, "application":${entry.application}, "user":<#if entry.user??>${entry.user}<#else>null, - "time":${entry.time?c}, + "time":"${xmldate(entry.time)}", "values": <#if entry.values??> { <#assign first=true> <#list entry.values?keys as k> <#if entry.values[k]??> - <#if !first>,<#else><#assign first=false>"${k}": - <#assign value = entry.values[k]>"${value}" + <#if !first>,<#else><#assign first=false>"${k}":<#assign value = entry.values[k]>"${value}" } @@ -23,3 +24,4 @@ ] } + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/web/scripts/audit/AbstractAuditWebScript.java b/source/java/org/alfresco/repo/web/scripts/audit/AbstractAuditWebScript.java index 912fbac976..313ad60227 100644 --- a/source/java/org/alfresco/repo/web/scripts/audit/AbstractAuditWebScript.java +++ b/source/java/org/alfresco/repo/web/scripts/audit/AbstractAuditWebScript.java @@ -38,6 +38,8 @@ public abstract class AbstractAuditWebScript extends DeclarativeWebScript public static final String PARAM_APPLICATION = "application"; public static final String PARAM_PATH="path"; public static final String PARAM_ENABLED = "enabled"; + public static final String PARAM_VALUE = "value"; + public static final String PARAM_VALUE_TYPE = "valueType"; public static final String PARAM_FROM_TIME = "fromTime"; public static final String PARAM_TO_TIME = "toTime"; public static final String PARAM_FROM_ID = "fromId"; @@ -62,12 +64,13 @@ public abstract class AbstractAuditWebScript extends DeclarativeWebScript public static final String JSON_KEY_PATH = "path"; public static final String JSON_KEY_CLEARED = "cleared"; + public static final String JSON_KEY_ENTRY_COUNT = "count"; public static final String JSON_KEY_ENTRIES = "entries"; - public static final String JSON_QUERY_KEY_ID = "id"; - public static final String JSON_QUERY_KEY_APPLICATION = "application"; - public static final String JSON_QUERY_KEY_USER = "user"; - public static final String JSON_QUERY_KEY_TIME = "time"; - public static final String JSON_QUERY_KEY_VALUES = "values"; + public static final String JSON_KEY_ENTRY_ID = "id"; + public static final String JSON_KEY_ENTRY_APPLICATION = "application"; + public static final String JSON_KEY_ENTRY_USER = "user"; + public static final String JSON_KEY_ENTRY_TIME = "time"; + public static final String JSON_KEY_ENTRY_VALUES = "values"; /** * Logger that can be used by subclasses. @@ -139,6 +142,19 @@ public abstract class AbstractAuditWebScript extends DeclarativeWebScript return Boolean.parseBoolean(enableStr); } + protected String getParamValue(WebScriptRequest req) + { + return req.getParameter(PARAM_VALUE); + } + + protected String getParamValueType(WebScriptRequest req) + { + return req.getParameter(PARAM_VALUE_TYPE); + } + + /** + * @see #DEFAULT_FROM_TIME + */ protected Long getParamFromTime(WebScriptRequest req) { String paramStr = req.getParameter(PARAM_FROM_TIME); @@ -152,6 +168,9 @@ public abstract class AbstractAuditWebScript extends DeclarativeWebScript } } + /** + * @see #DEFAULT_TO_TIME + */ protected Long getParamToTime(WebScriptRequest req) { String paramStr = req.getParameter(PARAM_TO_TIME); @@ -165,6 +184,9 @@ public abstract class AbstractAuditWebScript extends DeclarativeWebScript } } + /** + * @see #DEFAULT_FROM_ID + */ protected Long getParamFromId(WebScriptRequest req) { String paramStr = req.getParameter(PARAM_FROM_ID); @@ -178,6 +200,9 @@ public abstract class AbstractAuditWebScript extends DeclarativeWebScript } } + /** + * @see #DEFAULT_TO_ID + */ protected Long getParamToId(WebScriptRequest req) { String paramStr = req.getParameter(PARAM_TO_ID); @@ -191,11 +216,17 @@ public abstract class AbstractAuditWebScript extends DeclarativeWebScript } } + /** + * @see #DEFAULT_USER + */ protected String getParamUser(WebScriptRequest req) { return req.getParameter(PARAM_USER); } + /** + * @see #DEFAULT_FORWARD + */ protected boolean getParamForward(WebScriptRequest req) { String paramStr = req.getParameter(PARAM_FORWARD); @@ -206,6 +237,9 @@ public abstract class AbstractAuditWebScript extends DeclarativeWebScript return Boolean.parseBoolean(paramStr); } + /** + * @see #DEFAULT_LIMIT + */ protected int getParamLimit(WebScriptRequest req) { String paramStr = req.getParameter(PARAM_LIMIT); @@ -219,6 +253,9 @@ public abstract class AbstractAuditWebScript extends DeclarativeWebScript } } + /** + * @see #DEFAULT_VERBOSE + */ protected boolean getParamVerbose(WebScriptRequest req) { String paramStr = req.getParameter(PARAM_VERBOSE); diff --git a/source/java/org/alfresco/repo/web/scripts/audit/AuditQueryGet.java b/source/java/org/alfresco/repo/web/scripts/audit/AuditQueryGet.java index 74646693ee..7eed902623 100644 --- a/source/java/org/alfresco/repo/web/scripts/audit/AuditQueryGet.java +++ b/source/java/org/alfresco/repo/web/scripts/audit/AuditQueryGet.java @@ -20,6 +20,7 @@ package org.alfresco.repo.web.scripts.audit; import java.io.Serializable; import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,6 +28,7 @@ import java.util.Map; import org.alfresco.service.cmr.audit.AuditQueryParameters; import org.alfresco.service.cmr.audit.AuditService.AuditApplication; import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.springframework.extensions.webscripts.Cache; import org.springframework.extensions.webscripts.Status; import org.springframework.extensions.webscripts.WebScriptException; @@ -44,6 +46,10 @@ public class AuditQueryGet extends AbstractAuditWebScript final Map model = new HashMap(7); String appName = getParamAppName(req); + String path = getParamPath(req); + + Serializable value = getParamValue(req); + String valueType = getParamValueType(req); Long fromTime = getParamFromTime(req); Long toTime = getParamToTime(req); Long fromId = getParamFromId(req); @@ -63,6 +69,25 @@ public class AuditQueryGet extends AbstractAuditWebScript } } + // Transform the value to the correct type + if (value != null && valueType != null) + { + try + { + @SuppressWarnings("unchecked") + Class clazz = (Class) Class.forName(valueType); + value = DefaultTypeConverter.INSTANCE.convert(clazz, value); + } + catch (ClassNotFoundException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "audit.err.value.classNotFound", valueType); + } + catch (Throwable e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "audit.err.value.convertFailed", value, valueType); + } + } + // Execute the query AuditQueryParameters params = new AuditQueryParameters(); params.setApplicationName(appName); @@ -72,6 +97,10 @@ public class AuditQueryGet extends AbstractAuditWebScript params.setToId(toId); params.setUser(user); params.setForward(forward); + if (path != null || value != null) + { + params.addSearchKey(path, value); + } final List> entries = new ArrayList>(limit); AuditQueryCallback callback = new AuditQueryCallback() @@ -97,16 +126,16 @@ public class AuditQueryGet extends AbstractAuditWebScript Map values) { Map entry = new HashMap(11); - entry.put(JSON_QUERY_KEY_ID, entryId); - entry.put(JSON_QUERY_KEY_APPLICATION, applicationName); + entry.put(JSON_KEY_ENTRY_ID, entryId); + entry.put(JSON_KEY_ENTRY_APPLICATION, applicationName); if (user != null) { - entry.put(JSON_QUERY_KEY_USER, user); + entry.put(JSON_KEY_ENTRY_USER, user); } - entry.put(JSON_QUERY_KEY_TIME, new Long(time)); + entry.put(JSON_KEY_ENTRY_TIME, new Date(time)); if (values != null) { - entry.put(JSON_QUERY_KEY_VALUES, values); + entry.put(JSON_KEY_ENTRY_VALUES, values); } entries.add(entry); @@ -116,6 +145,7 @@ public class AuditQueryGet extends AbstractAuditWebScript auditService.auditQuery(callback, params, limit); + model.put(JSON_KEY_ENTRY_COUNT, entries.size()); model.put(JSON_KEY_ENTRIES, entries); // Done diff --git a/source/java/org/alfresco/repo/web/scripts/audit/AuditWebScriptTest.java b/source/java/org/alfresco/repo/web/scripts/audit/AuditWebScriptTest.java index 605e174fdf..4512713f3d 100644 --- a/source/java/org/alfresco/repo/web/scripts/audit/AuditWebScriptTest.java +++ b/source/java/org/alfresco/repo/web/scripts/audit/AuditWebScriptTest.java @@ -18,6 +18,7 @@ */ package org.alfresco.repo.web.scripts.audit; +import java.util.Date; import java.util.Map; import org.alfresco.repo.content.MimetypeMap; @@ -31,6 +32,7 @@ import org.alfresco.service.cmr.security.AuthenticationService; import org.json.JSONArray; import org.json.JSONObject; import org.springframework.context.ApplicationContext; +import org.springframework.extensions.surf.util.ISO8601DateFormat; import org.springframework.extensions.webscripts.Status; import org.springframework.extensions.webscripts.TestWebScriptServer; import org.springframework.extensions.webscripts.TestWebScriptServer.Response; @@ -212,7 +214,7 @@ public class AuditWebScriptTest extends BaseWebScriptTest /** * Perform a failed login attempt */ - private void loginWithFailure() throws Exception + private void loginWithFailure(final String username) throws Exception { // Force a failed login RunAsWork failureWork = new RunAsWork() @@ -222,7 +224,7 @@ public class AuditWebScriptTest extends BaseWebScriptTest { try { - authenticationService.authenticate("domino", "crud".toCharArray()); + authenticationService.authenticate(username, "crud".toCharArray()); fail("Failed to force authentication failure"); } catch (AuthenticationException e) @@ -240,7 +242,7 @@ public class AuditWebScriptTest extends BaseWebScriptTest long now = System.currentTimeMillis(); long future = Long.MAX_VALUE; - loginWithFailure(); + loginWithFailure(getName()); // Delete audit entries that could not have happened String url = "/api/audit/clear/" + APP_REPO_NAME + "?fromTime=" + future; @@ -249,15 +251,24 @@ public class AuditWebScriptTest extends BaseWebScriptTest JSONObject json = new JSONObject(response.getContentAsString()); int cleared = json.getInt(AbstractAuditWebScript.JSON_KEY_CLEARED); assertEquals("Could not have cleared more than 0", 0, cleared); - + + // Delete the entry (at least) url = "/api/audit/clear/" + APP_REPO_NAME + "?fromTime=" + now + "&toTime=" + future; req = new TestWebScriptServer.PostRequest(url, "", MimetypeMap.MIMETYPE_JSON); response = sendRequest(req, Status.STATUS_OK, admin); json = new JSONObject(response.getContentAsString()); cleared = json.getInt(AbstractAuditWebScript.JSON_KEY_CLEARED); assertTrue("Should have cleared at least 1 entry", cleared > 0); + + // Delete all entries + url = "/api/audit/clear/" + APP_REPO_NAME;; + req = new TestWebScriptServer.PostRequest(url, "", MimetypeMap.MIMETYPE_JSON); + response = sendRequest(req, Status.STATUS_OK, admin); + json = new JSONObject(response.getContentAsString()); + cleared = json.getInt(AbstractAuditWebScript.JSON_KEY_CLEARED); } + @SuppressWarnings("unused") public void testQueryAuditRepo() throws Exception { long now = System.currentTimeMillis(); @@ -266,14 +277,98 @@ public class AuditWebScriptTest extends BaseWebScriptTest auditService.setAuditEnabled(true); auditService.enableAudit(APP_REPO_NAME, APP_REPO_PATH); - loginWithFailure(); + loginWithFailure(getName()); - // Delete audit entries that could not have happened + // Query for audit entries that could not have happened String url = "/api/audit/query/" + APP_REPO_NAME + "?fromTime=" + now + "&verbose=true"; TestWebScriptServer.GetRequest req = new TestWebScriptServer.GetRequest(url); Response response = sendRequest(req, Status.STATUS_OK, admin); JSONObject json = new JSONObject(response.getContentAsString()); + Long entryCount = json.getLong(AbstractAuditWebScript.JSON_KEY_ENTRY_COUNT); JSONArray jsonEntries = json.getJSONArray(AbstractAuditWebScript.JSON_KEY_ENTRIES); assertTrue("Expected at least one entry", jsonEntries.length() > 0); + assertEquals("Entry count and physical count don't match", new Long(jsonEntries.length()), entryCount); + JSONObject jsonEntry = jsonEntries.getJSONObject(0); + Long entryId = jsonEntry.getLong(AbstractAuditWebScript.JSON_KEY_ENTRY_ID); + assertNotNull("No entry ID", entryId); + String entryTimeStr = jsonEntry.getString(AbstractAuditWebScript.JSON_KEY_ENTRY_TIME); + assertNotNull("No entry time String", entryTimeStr); + Date entryTime = ISO8601DateFormat.parse((String)entryTimeStr); // Check conversion + JSONObject jsonValues = jsonEntry.getJSONObject(AbstractAuditWebScript.JSON_KEY_ENTRY_VALUES); + String entryUsername = jsonValues.getString("/repository/login/error/user"); + assertEquals("Didn't find the login-failure-user", getName(), entryUsername); + + // Query using well-known ID + Long fromEntryId = entryId; // Search is inclusive on the 'from' side + Long toEntryId = entryId.longValue() + 1L; // Search is exclusive on the 'to' side + url = "/api/audit/query/" + APP_REPO_NAME + "?fromId=" + fromEntryId + "&toId=" + toEntryId; + req = new TestWebScriptServer.GetRequest(url); + response = sendRequest(req, Status.STATUS_OK, admin); + json = new JSONObject(response.getContentAsString()); + jsonEntries = json.getJSONArray(AbstractAuditWebScript.JSON_KEY_ENTRIES); + assertEquals("Incorrect number of search results", 1, jsonEntries.length()); + + // Query using a non-existent entry path + url = "/api/audit/query/" + APP_REPO_NAME + "/repository/login/error/userXXX" + "?verbose=true"; + req = new TestWebScriptServer.GetRequest(url); + response = sendRequest(req, Status.STATUS_OK, admin); + json = new JSONObject(response.getContentAsString()); + jsonEntries = json.getJSONArray(AbstractAuditWebScript.JSON_KEY_ENTRIES); + assertTrue("Should not have found anything", jsonEntries.length() == 0); + + // Query using a good entry path + url = "/api/audit/query/" + APP_REPO_NAME + "/repository/login/error/user" + "?verbose=true"; + req = new TestWebScriptServer.GetRequest(url); + response = sendRequest(req, Status.STATUS_OK, admin); + json = new JSONObject(response.getContentAsString()); + jsonEntries = json.getJSONArray(AbstractAuditWebScript.JSON_KEY_ENTRIES); + assertTrue("Should have found entries", jsonEntries.length() > 0); + + // Now login with failure using a GUID and ensure that we can find it + String missingUser = new Long(System.currentTimeMillis()).toString(); + + // Query for event that has not happened + url = "/api/audit/query/" + APP_REPO_NAME + "/repository/login/error/user" + "?value=" + missingUser; + req = new TestWebScriptServer.GetRequest(url); + response = sendRequest(req, Status.STATUS_OK, admin); + json = new JSONObject(response.getContentAsString()); + jsonEntries = json.getJSONArray(AbstractAuditWebScript.JSON_KEY_ENTRIES); + assertEquals("Incorrect number of search results", 0, jsonEntries.length()); + + loginWithFailure(missingUser); + + // Query for event that has happened once + url = "/api/audit/query/" + APP_REPO_NAME + "/repository/login/error/user" + "?value=" + missingUser; + req = new TestWebScriptServer.GetRequest(url); + response = sendRequest(req, Status.STATUS_OK, admin); + json = new JSONObject(response.getContentAsString()); + jsonEntries = json.getJSONArray(AbstractAuditWebScript.JSON_KEY_ENTRIES); + assertEquals("Incorrect number of search results", 1, jsonEntries.length()); + + // Query for event, but casting the value to the incorrect type + url = "/api/audit/query/" + APP_REPO_NAME + "/repository/login/error/user" + "?value=" + missingUser + "&valueType=java.lang.Long"; + req = new TestWebScriptServer.GetRequest(url); + response = sendRequest(req, Status.STATUS_OK, admin); + json = new JSONObject(response.getContentAsString()); + jsonEntries = json.getJSONArray(AbstractAuditWebScript.JSON_KEY_ENTRIES); + assertEquals("Incorrect number of search results", 0, jsonEntries.length()); + + // Test what happens when the target data needs encoding + String oddUser = "%$£\\\"\'"; + loginWithFailure(oddUser); + + // Query for the event limiting to one by count and descending (i.e. get last) + url = "/api/audit/query/" + APP_REPO_NAME + "?forward=false&limit=1&verbose=true"; + req = new TestWebScriptServer.GetRequest(url); + response = sendRequest(req, Status.STATUS_OK, admin); + json = new JSONObject(response.getContentAsString()); + jsonEntries = json.getJSONArray(AbstractAuditWebScript.JSON_KEY_ENTRIES); + assertEquals("Incorrect number of search results", 1, jsonEntries.length()); + jsonEntry = jsonEntries.getJSONObject(0); + entryId = jsonEntry.getLong(AbstractAuditWebScript.JSON_KEY_ENTRY_ID); + assertNotNull("No entry ID", entryId); + jsonValues = jsonEntry.getJSONObject(AbstractAuditWebScript.JSON_KEY_ENTRY_VALUES); + entryUsername = jsonValues.getString("/repository/login/error/user"); + assertEquals("Didn't find the login-failure-user", oddUser, entryUsername); } }