diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml index e4602e6d8c..fe7ef4f68e 100644 --- a/config/alfresco/public-rest-context.xml +++ b/config/alfresco/public-rest-context.xml @@ -44,7 +44,7 @@ - + Public Api diff --git a/config/alfresco/web-scripts-application-context.xml b/config/alfresco/web-scripts-application-context.xml index e11b6ec737..44b46b1fef 100644 --- a/config/alfresco/web-scripts-application-context.xml +++ b/config/alfresco/web-scripts-application-context.xml @@ -134,7 +134,16 @@ js - + + + + java.sql.SQLException + org.alfresco.service.cmr.repository.ContentIOException + + + + + Repository diff --git a/source/java/org/alfresco/repo/web/scripts/RepositoryContainer.java b/source/java/org/alfresco/repo/web/scripts/RepositoryContainer.java index 2cf5baffaa..5246eeb180 100644 --- a/source/java/org/alfresco/repo/web/scripts/RepositoryContainer.java +++ b/source/java/org/alfresco/repo/web/scripts/RepositoryContainer.java @@ -21,8 +21,8 @@ package org.alfresco.repo.web.scripts; import java.io.File; import java.io.IOException; import java.net.SocketException; -import java.sql.SQLException; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletResponse; @@ -90,7 +90,7 @@ public class RepositoryContainer extends AbstractRuntimeContainer private long maxContentSize = (long) 4 * 1024 * 1024 * 1024; // 4gb private ThresholdOutputStreamFactory streamFactory = null; - private final static Class[] HIDE_EXCEPTIONS = new Class[] { SQLException.class }; + private Class[] notPublicExceptions = new Class[] {}; /* * Shame init is already used (by TenantRepositoryContainer). @@ -178,6 +178,25 @@ public class RepositoryContainer extends AbstractRuntimeContainer this.authorityService = authorityService; } + /** + * Exceptions which may contain information that cannot be displayed in UI + * + * @param notPublicExceptions - {@link Class}<?>[] instance which contains list of not public exceptions + */ + public void setNotPublicExceptions(List> notPublicExceptions) + { + this.notPublicExceptions = new Class[] {}; + if((null != notPublicExceptions) && !notPublicExceptions.isEmpty()) + { + this.notPublicExceptions = notPublicExceptions.toArray(this.notPublicExceptions); + } + } + + public Class[] getNotPublicExceptions() + { + return notPublicExceptions; + } + /* (non-Javadoc) * @see org.alfresco.web.scripts.Container#getDescription() */ @@ -262,7 +281,7 @@ public class RepositoryContainer extends AbstractRuntimeContainer } catch (RuntimeException e) { - Throwable hideCause = ExceptionStackUtil.getCause(e, HIDE_EXCEPTIONS); + Throwable hideCause = ExceptionStackUtil.getCause(e, notPublicExceptions); if (hideCause != null) { AlfrescoRuntimeException alf = null; diff --git a/source/test-java/org/alfresco/repo/web/scripts/RepositoryContainerTest.java b/source/test-java/org/alfresco/repo/web/scripts/RepositoryContainerTest.java index b6967f6284..04ef309aaf 100644 --- a/source/test-java/org/alfresco/repo/web/scripts/RepositoryContainerTest.java +++ b/source/test-java/org/alfresco/repo/web/scripts/RepositoryContainerTest.java @@ -18,9 +18,15 @@ */ package org.alfresco.repo.web.scripts; +import static org.mockito.Matchers.any; +import static org.springframework.extensions.webscripts.Status.STATUS_OK; + import java.sql.SQLException; -import java.util.regex.Pattern; import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Pattern; import javax.servlet.http.HttpServletResponse; @@ -30,6 +36,7 @@ import org.alfresco.model.ContentModel; import org.alfresco.repo.forms.FormException; import org.alfresco.repo.security.authentication.AuthenticationComponent; import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.security.MutableAuthenticationService; import org.alfresco.service.cmr.security.PersonService; import org.alfresco.util.CronTriggerBean; @@ -37,18 +44,14 @@ import org.alfresco.util.PropertyMap; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.springframework.extensions.webscripts.Authenticator; import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; import org.springframework.extensions.webscripts.TestWebScriptServer.PutRequest; import org.springframework.extensions.webscripts.TestWebScriptServer.Response; -import org.springframework.extensions.webscripts.Authenticator; import org.springframework.extensions.webscripts.WebScriptException; import org.springframework.extensions.webscripts.WebScriptRequest; import org.springframework.extensions.webscripts.WebScriptResponse; -import static org.springframework.extensions.webscripts.Status.*; - -import static org.mockito.Matchers.any; - /** * Unit test to test runas function * @@ -56,6 +59,8 @@ import static org.mockito.Matchers.any; */ public class RepositoryContainerTest extends BaseWebScriptTest { + private static final Pattern HIDDEN_EXCEPTION_PATTERN = Pattern.compile("Server error \\(\\d{8}\\)\\. Details can be found in the server logs\\."); + private MutableAuthenticationService authenticationService; private PersonService personService; private AuthenticationComponent authenticationComponent; @@ -162,106 +167,273 @@ public class RepositoryContainerTest extends BaseWebScriptTest assertEquals(SUCCESS, response.getContentAsString()); } - public void testHideExceptions() throws Exception { - final Pattern patternHiddenException = Pattern.compile("Server error \\(\\d{8}\\)\\. Details can be found in the server logs\\."); final String messageFormException = "Failed to persist field 'prop_cm_name'"; final String messageAuthenticationException = "Authentication failed for Web Script"; - + RepositoryContainer repoContainer = (RepositoryContainer) getServer().getApplicationContext().getBean("webscripts.container"); RepositoryContainer repoContainerMock = Mockito.spy(repoContainer); - // case: AlfrescoRuntimeException with SQLException cause - Mockito.doAnswer(new Answer() - { - public Object answer(InvocationOnMock invocation) - { - throw new AlfrescoRuntimeException("AlfrescoRuntimeException", new SQLException("SQLException")); - } - }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); - try - { - repoContainerMock.executeScript(null, null, null); - } - catch (Exception e) - { - assertNull("SQLException cause should be hidden for client", ExceptionStackUtil.getCause(e, new Class[] { SQLException.class })); - assertTrue("Details should be in the server logs.", patternHiddenException.matcher(e.getMessage()).matches()); - } + Class[] defaultConfiguration = repoContainerMock.getNotPublicExceptions(); - // case: AlfrescoRuntimeException with NOT SQLException cause - Mockito.doAnswer(new Answer() - { - public Object answer(InvocationOnMock invocation) - { - throw new AlfrescoRuntimeException("AlfrescoRuntimeException", new NullPointerException()); - } - }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); try { - repoContainerMock.executeScript(null, null, null); - } - catch (Exception e) - { - assertNotNull("NullPointerException cause should be visible for client", ExceptionStackUtil.getCause(e, new Class[] { NullPointerException.class })); - assertFalse("Details should be available for client", patternHiddenException.matcher(e.getMessage()).matches()); - } + List> testExceptoins = new LinkedList>(); + testExceptoins.add(SQLException.class); + repoContainerMock.setNotPublicExceptions(testExceptoins); - // case: RuntimeException with SQLException cause - Mockito.doAnswer(new Answer() - { - public Object answer(InvocationOnMock invocation) + // case: AlfrescoRuntimeException with SQLException cause + Mockito.doAnswer(new Answer() { - throw new RuntimeException("AlfrescoRuntimeException", new SQLException("SQLException")); + public Object answer(InvocationOnMock invocation) + { + throw new AlfrescoRuntimeException("AlfrescoRuntimeException", new SQLException("SQLException")); + } + }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); + try + { + repoContainerMock.executeScript(null, null, null); + } + catch (Exception e) + { + assertNull("SQLException cause should be hidden for client", ExceptionStackUtil.getCause(e, new Class[] { SQLException.class })); + assertTrue("Details should be in the server logs.", HIDDEN_EXCEPTION_PATTERN.matcher(e.getMessage()).matches()); } - }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); - try - { - repoContainerMock.executeScript(null, null, null); - } - catch (Exception e) - { - assertNull("SQLException cause should be hidden for client", ExceptionStackUtil.getCause(e, new Class[] { SQLException.class })); - assertTrue("Details should be in the server logs.", patternHiddenException.matcher(e.getMessage()).matches()); - } - // case: FormException - Mockito.doAnswer(new Answer() - { - public Object answer(InvocationOnMock invocation) + // case: AlfrescoRuntimeException with NOT SQLException cause + Mockito.doAnswer(new Answer() { - throw new FormException(messageFormException); + public Object answer(InvocationOnMock invocation) + { + throw new AlfrescoRuntimeException("AlfrescoRuntimeException", new NullPointerException()); + } + }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); + try + { + repoContainerMock.executeScript(null, null, null); + } + catch (Exception e) + { + assertNotNull("NullPointerException cause should be visible for client", ExceptionStackUtil.getCause(e, new Class[] { NullPointerException.class })); + assertFalse("Details should be available for client", HIDDEN_EXCEPTION_PATTERN.matcher(e.getMessage()).matches()); } - }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); - try - { - repoContainerMock.executeScript(null, null, null); - } - catch (Exception e) - { - assertTrue("FormException should be visible for client", e instanceof FormException); - assertFalse("Details should be available for client", patternHiddenException.matcher(e.getMessage()).matches()); - assertTrue("Actual message should be available for client", e.getMessage().contains(messageFormException)); - } - // case: WebScriptException - Mockito.doAnswer(new Answer() - { - public Object answer(InvocationOnMock invocation) + // case: RuntimeException with SQLException cause + Mockito.doAnswer(new Answer() { - throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, messageAuthenticationException); + public Object answer(InvocationOnMock invocation) + { + throw new RuntimeException("AlfrescoRuntimeException", new SQLException("SQLException")); + } + }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); + try + { + repoContainerMock.executeScript(null, null, null); } - }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); + catch (Exception e) + { + assertNull("SQLException cause should be hidden for client", ExceptionStackUtil.getCause(e, new Class[] { SQLException.class })); + assertTrue("Details should be in the server logs.", HIDDEN_EXCEPTION_PATTERN.matcher(e.getMessage()).matches()); + } + + // case: FormException + Mockito.doAnswer(new Answer() + { + public Object answer(InvocationOnMock invocation) + { + throw new FormException(messageFormException); + } + }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); + try + { + repoContainerMock.executeScript(null, null, null); + } + catch (Exception e) + { + assertTrue("FormException should be visible for client", e instanceof FormException); + assertFalse("Details should be available for client", HIDDEN_EXCEPTION_PATTERN.matcher(e.getMessage()).matches()); + assertTrue("Actual message should be available for client", e.getMessage().contains(messageFormException)); + } + + // case: WebScriptException + Mockito.doAnswer(new Answer() + { + public Object answer(InvocationOnMock invocation) + { + throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, messageAuthenticationException); + } + }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); + try + { + repoContainerMock.executeScript(null, null, null); + } + catch (Exception e) + { + assertTrue("WebScriptException should be visible for client", e instanceof WebScriptException); + assertFalse("Details should be available for client", HIDDEN_EXCEPTION_PATTERN.matcher(e.getMessage()).matches()); + assertTrue("Actual message should be available for client", e.getMessage().contains(messageAuthenticationException)); + } + } + finally + { + repoContainerMock.setNotPublicExceptions(Arrays.asList(defaultConfiguration)); + } + } + + public void testHideExceptionsConfiguration() throws Exception + { + final StringBuilder commandExecutionResult = new StringBuilder(128); + commandExecutionResult.append("Execution result: \n").append(" os: ").append(System.getProperty("os.name")).append("\n").append(" command: ") + .append(" succeeded: ").append(false).append("\n").append(" exit code: ").append(1).append("\n").append(" out: ").append("").append("\n") + .append(" err: ").append("ERROR"); + + RepositoryContainer repoContainer = (RepositoryContainer) getServer().getApplicationContext().getBean("webscripts.container"); + RepositoryContainer repoContainerMock = Mockito.spy(repoContainer); + + Class[] notPublicExceptions = repoContainerMock.getNotPublicExceptions(); + List> defaultConfiguration = Arrays.asList(notPublicExceptions); + + List> testExceptions = new LinkedList>(); + testExceptions.add(SQLException.class); + testExceptions.add(ContentIOException.class); + repoContainerMock.setNotPublicExceptions(testExceptions); + try { - repoContainerMock.executeScript(null, null, null); + // Case: MNT-12794 - test default configuration + Mockito.doAnswer(new Answer() + { + public Object answer(InvocationOnMock invocation) + { + throw new ContentIOException(commandExecutionResult.toString()); + } + }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); + + try + { + repoContainerMock.executeScript(null, null, null); + } + catch (Exception e) + { + assertNull("'ContentIOException' cause should be hidden for client", ExceptionStackUtil.getCause(e, new Class[] { ContentIOException.class })); + assertTrue("Details should be in the server logs.", HIDDEN_EXCEPTION_PATTERN.matcher(e.getMessage()).matches()); + } + + // Case: MNT-12794 - ContentIOException in AlfrescoRuntimeException and default configuration + Mockito.doAnswer(new Answer() + { + public Object answer(InvocationOnMock invocation) + { + + throw new AlfrescoRuntimeException("AlfrescoRuntimeException", new ContentIOException(commandExecutionResult.toString())); + } + }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); + + try + { + repoContainerMock.executeScript(null, null, null); + } + catch (Exception e) + { + assertNull("'ContentIOException' cause should be hidden for client", ExceptionStackUtil.getCause(e, new Class[] { ContentIOException.class })); + assertTrue("Details should be in the server logs.", HIDDEN_EXCEPTION_PATTERN.matcher(e.getMessage()).matches()); + } + + // case: AlfrescoRuntimeException without ContentIOException + Mockito.doAnswer(new Answer() + { + public Object answer(InvocationOnMock invocation) + { + throw new AlfrescoRuntimeException("AlfrescoRuntimeException", new NullPointerException("NullPointerException")); + } + }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); + + try + { + repoContainerMock.executeScript(null, null, null); + } + catch (Exception e) + { + assertNotNull("'NullPointerException' cause should be visible for client", ExceptionStackUtil.getCause(e, new Class[] { NullPointerException.class })); + assertFalse("Details should be available for client", HIDDEN_EXCEPTION_PATTERN.matcher(e.getMessage()).matches()); + } + + // case: RuntimeException with several different exceptions which should be shown + List> updatedNotPublicExceptions = new LinkedList>(defaultConfiguration); + updatedNotPublicExceptions.add(NullPointerException.class); + repoContainerMock.setNotPublicExceptions(updatedNotPublicExceptions); + + Mockito.doAnswer(new Answer() + { + public Object answer(InvocationOnMock invocation) + { + throw new AlfrescoRuntimeException("AlfrescoRuntimeException", new NullPointerException("NullPointerException")); + } + }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); + + try + { + repoContainerMock.executeScript(null, null, null); + } + catch (Exception e) + { + assertNull("'NullPointerException' cause should be hidden for client", ExceptionStackUtil.getCause(e, new Class[] { NullPointerException.class })); + assertTrue("Details should be in the server logs.", HIDDEN_EXCEPTION_PATTERN.matcher(e.getMessage()).matches()); + } + + // case: all exceptions must be shown + repoContainerMock.setNotPublicExceptions(null); + + Mockito.doAnswer(new Answer() + { + public Object answer(InvocationOnMock invocation) + { + throw new AlfrescoRuntimeException("AlfrescoRuntimeException", new ContentIOException(commandExecutionResult.toString(), new NullPointerException( + "NullPointerException"))); + } + }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); + + try + { + repoContainerMock.executeScript(null, null, null); + } + catch (Exception e) + { + assertTrue("'AlfrescoRuntimeException' cause should not be hidden for client. Exception class: " + e.getClass().getName(), e instanceof AlfrescoRuntimeException); + assertNotNull("'ContentIOException' cause should not be hidden for client", ExceptionStackUtil.getCause(e, new Class[] { ContentIOException.class })); + assertNotNull("'NullPointerException' cause should not be hidden for client", ExceptionStackUtil.getCause(e, new Class[] { NullPointerException.class })); + assertFalse("Details should be available for client", HIDDEN_EXCEPTION_PATTERN.matcher(e.getMessage()).matches()); + } + + // case: all exceptions must be shown + repoContainerMock.setNotPublicExceptions(defaultConfiguration); + repoContainerMock.setNotPublicExceptions(new LinkedList>()); + + Mockito.doAnswer(new Answer() + { + public Object answer(InvocationOnMock invocation) + { + throw new AlfrescoRuntimeException("AlfrescoRuntimeException", new ContentIOException(commandExecutionResult.toString(), new NullPointerException( + "NullPointerException"))); + } + }).when(repoContainerMock).executeScriptInternal(any(WebScriptRequest.class), any(WebScriptResponse.class), any(Authenticator.class)); + + try + { + repoContainerMock.executeScript(null, null, null); + } + catch (Exception e) + { + assertTrue("'AlfrescoRuntimeException' cause should not be hidden for client. Exception class: " + e.getClass().getName(), e instanceof AlfrescoRuntimeException); + assertNotNull("'ContentIOException' cause should not be hidden for client", ExceptionStackUtil.getCause(e, new Class[] { ContentIOException.class })); + assertNotNull("'NullPointerException' cause should not be hidden for client", ExceptionStackUtil.getCause(e, new Class[] { NullPointerException.class })); + assertFalse("Details should be available for client", HIDDEN_EXCEPTION_PATTERN.matcher(e.getMessage()).matches()); + } } - catch (Exception e) + finally { - assertTrue("WebScriptException should be visible for client", e instanceof WebScriptException); - assertFalse("Details should be available for client", patternHiddenException.matcher(e.getMessage()).matches()); - assertTrue("Actual message should be available for client", e.getMessage().contains(messageAuthenticationException)); + repoContainerMock.setNotPublicExceptions(defaultConfiguration); } } }