From 84d3cadb1292414b5f33e9866d97f757c246ce3b Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Sat, 15 Mar 2014 15:40:55 +0100 Subject: [PATCH] prompt authentication again for failed subversion authentication and improved error message for missing privileges --- .../main/java/sonia/scm/util/HttpUtil.java | 68 ++++++++++-- .../sonia/scm/repository/ScmSvnErrorCode.java | 71 ++++++++++++ .../java/sonia/scm/repository/SvnUtil.java | 103 +++++++++++++++++ .../scm/web/SvnBasicAuthenticationFilter.java | 105 ++++++++++++++++++ .../sonia/scm/web/SvnPermissionFilter.java | 46 ++++++-- .../java/sonia/scm/web/SvnServletModule.java | 2 +- 6 files changed, 377 insertions(+), 18 deletions(-) create mode 100644 scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/ScmSvnErrorCode.java create mode 100644 scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java diff --git a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java index 150d1a3ba4..c8717c1cb7 100644 --- a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java @@ -37,6 +37,7 @@ package sonia.scm.util; import com.google.common.base.CharMatcher; import com.google.common.base.Strings; +import com.google.common.io.ByteStreams; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +47,7 @@ import sonia.scm.config.ScmConfiguration; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; +import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -158,6 +160,9 @@ public final class HttpUtil public static final String STATUS_UNAUTHORIZED_MESSAGE = "Authorization Required"; + /** Field description */ + private static final int SKIP_SIZE = 4096; + /** the logger for HttpUtil */ private static final Logger logger = LoggerFactory.getLogger(HttpUtil.class); @@ -167,7 +172,7 @@ public final class HttpUtil */ private static final Pattern PATTERN_URLNORMALIZE = Pattern.compile("(?:(http://[^:]+):80(/.+)?|(https://[^:]+):443(/.+)?)"); - + /** * CharMatcher to select cr/lf and '%' characters * @since 1.28 @@ -197,10 +202,10 @@ public final class HttpUtil */ public static String append(String uri, String suffix) { - if ( uri.endsWith(SEPARATOR_PATH) && suffix.startsWith(SEPARATOR_PATH) ) + if (uri.endsWith(SEPARATOR_PATH) && suffix.startsWith(SEPARATOR_PATH)) { - uri = uri.substring( 0, uri.length() - 1 ); - } + uri = uri.substring(0, uri.length() - 1); + } else if (!uri.endsWith(SEPARATOR_PATH) &&!suffix.startsWith(SEPARATOR_PATH)) { uri = uri.concat(SEPARATOR_PATH); @@ -299,6 +304,38 @@ public final class HttpUtil return value; } + /** + * Skips to complete body of a request. + * + * + * @param request http request + * + * @since 1.37 + */ + public static void drainBody(HttpServletRequest request) + { + if (isChunked(request) || (request.getContentLength() > 0)) + { + InputStream in = null; + + try + { + in = request.getInputStream(); + + while ((0 < in.skip(SKIP_SIZE)) || (0 <= in.read())) + { + + // nothing + } + } + catch (IOException e) {} + finally + { + IOUtil.close(in); + } + } + } + /** * Method description * @@ -443,10 +480,11 @@ public final class HttpUtil * @param realmDescription - realm description * * @throws IOException - * + * * @since 1.36 */ - public static void sendUnauthorized(HttpServletResponse response, String realmDescription) + public static void sendUnauthorized(HttpServletResponse response, + String realmDescription) throws IOException { sendUnauthorized(null, response, realmDescription); @@ -465,8 +503,7 @@ public final class HttpUtil * @since 1.19 */ public static void sendUnauthorized(HttpServletRequest request, - HttpServletResponse response, - String realmDescription) + HttpServletResponse response, String realmDescription) throws IOException { if ((request == null) ||!isWUIRequest(request)) @@ -671,6 +708,21 @@ public final class HttpUtil return uri; } + /** + * Returns true if the body of the request is chunked. + * + * + * @param request http request + * + * @return true if the request is chunked + * + * @since 1.37 + */ + public static boolean isChunked(HttpServletRequest request) + { + return "chunked".equals(request.getHeader("Transfer-Encoding")); + } + /** * Returns true if the http request is send by the scm-manager web interface. * diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/ScmSvnErrorCode.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/ScmSvnErrorCode.java new file mode 100644 index 0000000000..58df07c7de --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/ScmSvnErrorCode.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.repository; + +//~--- non-JDK imports -------------------------------------------------------- + +import org.tmatesoft.svn.core.SVNErrorCode; + +/** + * + * @author Sebastian Sdorra + */ +public class ScmSvnErrorCode extends SVNErrorCode +{ + + /** Field description */ + public static final SVNErrorCode AUTHN_FAILED = + new ScmSvnErrorCode(AUTHN_CATEGORY, 4, "Authentication failed"); + + /** Field description */ + public static final SVNErrorCode AUTHZ_NOT_ENOUGH_PRIVILEGES = + new ScmSvnErrorCode(AUTHZ_CATEGORY, 4, + "You do not have enough access privileges for this operation."); + + /** Field description */ + private static final long serialVersionUID = -6864996390796610410L; + + //~--- constructors --------------------------------------------------------- + + /** + * Constructs ... + * + * + * @param category + * @param index + * @param description + */ + protected ScmSvnErrorCode(int category, int index, String description) + { + super(category, index, description); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java index abe5689712..60521ccbd1 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java @@ -37,23 +37,37 @@ package sonia.scm.repository; import com.google.common.base.Strings; import com.google.common.collect.Lists; +import com.google.common.io.Closeables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNLogEntry; import org.tmatesoft.svn.core.SVNLogEntryPath; +import org.tmatesoft.svn.core.internal.io.dav.DAVElement; +import org.tmatesoft.svn.core.internal.server.dav.DAVXMLUtil; +import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil; +import org.tmatesoft.svn.core.internal.util.SVNXMLUtil; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.admin.SVNChangeEntry; +import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; //~--- JDK imports ------------------------------------------------------------ +import java.io.IOException; +import java.io.PrintWriter; + import java.util.List; +import java.util.Locale; import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + /** * * @author Sebastian Sdorra @@ -61,6 +75,12 @@ import java.util.Map; public final class SvnUtil { + /** Field description */ + public static final String XML_CONTENT_TYPE = "text/xml; charset=\"utf-8\""; + + /** Field description */ + private static final String HEADER_USERAGENT = "User-Agent"; + /** Field description */ private static final String ID_TRANSACTION_PREFIX = "-1:"; @@ -70,6 +90,9 @@ public final class SvnUtil */ private static final char TYPE_UPDATED = 'U'; + /** Field description */ + private static final String USERAGENT_SVN = "svn/"; + /** * the logger for SvnUtil */ @@ -232,6 +255,39 @@ public final class SvnUtil return changesets; } + /** + * Method description + * + * @param errorCode + * + * @return + */ + public static String createErrorBody(SVNErrorCode errorCode) + { + StringBuffer xmlBuffer = new StringBuffer(); + + SVNXMLUtil.addXMLHeader(xmlBuffer); + + List namespaces = Lists.newArrayList(DAVElement.DAV_NAMESPACE, + DAVElement.SVN_APACHE_PROPERTY_NAMESPACE); + + SVNXMLUtil.openNamespaceDeclarationTag(SVNXMLUtil.DAV_NAMESPACE_PREFIX, + DAVXMLUtil.SVN_DAV_ERROR_TAG, namespaces, SVNXMLUtil.PREFIX_MAP, + xmlBuffer); + + SVNXMLUtil.openXMLTag(SVNXMLUtil.SVN_APACHE_PROPERTY_PREFIX, + "human-readable", SVNXMLUtil.XML_STYLE_NORMAL, "errcode", + String.valueOf(errorCode.getCode()), xmlBuffer); + xmlBuffer.append( + SVNEncodingUtil.xmlEncodeCDATA(errorCode.getDescription())); + SVNXMLUtil.closeXMLTag(SVNXMLUtil.SVN_APACHE_PROPERTY_PREFIX, + "human-readable", xmlBuffer); + SVNXMLUtil.closeXMLTag(SVNXMLUtil.DAV_NAMESPACE_PREFIX, + DAVXMLUtil.SVN_DAV_ERROR_TAG, xmlBuffer); + + return xmlBuffer.toString(); + } + /** * Method description * @@ -266,6 +322,39 @@ public final class SvnUtil } } + /** + * Method description + * + * + * @param request + * @param response + * @param statusCode + * @param errorCode + * + * @throws IOException + */ + public static void sendError(HttpServletRequest request, + HttpServletResponse response, int statusCode, SVNErrorCode errorCode) + throws IOException + { + HttpUtil.drainBody(request); + + response.setStatus(statusCode); + response.setContentType(XML_CONTENT_TYPE); + + PrintWriter writer = null; + + try + { + writer = response.getWriter(); + writer.println(createErrorBody(errorCode)); + } + finally + { + Closeables.close(writer, true); + } + } + //~--- get methods ---------------------------------------------------------- /** @@ -311,6 +400,20 @@ public final class SvnUtil return id.substring(ID_TRANSACTION_PREFIX.length()); } + /** + * Method description + * + * + * @param request + * + * @return + */ + public static boolean isSvnClient(HttpServletRequest request) + { + return Strings.nullToEmpty(request.getHeader(HEADER_USERAGENT)).toLowerCase( + Locale.ENGLISH).startsWith(USERAGENT_SVN); + } + /** * Method description * diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java new file mode 100644 index 0000000000..383d081ff5 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.web; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.tmatesoft.svn.core.SVNErrorCode; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.ScmSvnErrorCode; +import sonia.scm.repository.SvnUtil; +import sonia.scm.web.filter.AutoLoginModule; +import sonia.scm.web.filter.BasicAuthenticationFilter; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; + +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import sonia.scm.util.HttpUtil; + +/** + * + * @author Sebastian Sdorra + */ +@Singleton +public class SvnBasicAuthenticationFilter extends BasicAuthenticationFilter +{ + + /** + * Constructs ... + * + * + * @param configuration + * @param autoLoginModules + */ + @Inject + public SvnBasicAuthenticationFilter(ScmConfiguration configuration, + Set autoLoginModules) + { + super(configuration, autoLoginModules); + } + + //~--- methods -------------------------------------------------------------- + + /** + * Sends unauthorized instead of forbidden for svn clients, because the + * svn client prompts again for authentication. + * + * + * @param request http request + * @param response http response + * + * @throws IOException + */ + @Override + protected void sendFailedAuthenticationError(HttpServletRequest request, + HttpServletResponse response) + throws IOException + { + if (SvnUtil.isSvnClient(request)) + { + HttpUtil.sendUnauthorized(response, configuration.getRealmDescription()); + } + else + { + super.sendFailedAuthenticationError(request, response); + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java index 164ccfd173..2d41e5d8ae 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java @@ -39,15 +39,22 @@ import com.google.common.collect.ImmutableSet; import com.google.inject.Inject; import com.google.inject.Singleton; +import org.tmatesoft.svn.core.SVNErrorCode; + import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.RepositoryProvider; +import sonia.scm.repository.ScmSvnErrorCode; +import sonia.scm.repository.SvnUtil; import sonia.scm.web.filter.ProviderPermissionFilter; //~--- JDK imports ------------------------------------------------------------ +import java.io.IOException; + import java.util.Set; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; /** * @@ -58,22 +65,16 @@ public class SvnPermissionFilter extends ProviderPermissionFilter { /** Field description */ - private static Set WRITEMETHOD_SET = ImmutableSet.of("MKACTIVITY", - "PROPPATCH", "PUT", - "CHECKOUT", "MKCOL", "MOVE", - "COPY", "DELETE", "LOCK", - "UNLOCK", "MERGE"); + private static final Set WRITEMETHOD_SET = + ImmutableSet.of("MKACTIVITY", "PROPPATCH", "PUT", "CHECKOUT", "MKCOL", + "MOVE", "COPY", "DELETE", "LOCK", "UNLOCK", "MERGE"); //~--- constructors --------------------------------------------------------- /** * Constructs ... * - * - * - * * @param configuration - * @param securityContextProvider * @param repository */ @Inject @@ -83,6 +84,33 @@ public class SvnPermissionFilter extends ProviderPermissionFilter super(configuration, repository); } + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param request + * @param response + * + * @throws IOException + */ + @Override + protected void sendNotEnoughPrivilegesError(HttpServletRequest request, + HttpServletResponse response) + throws IOException + { + if (SvnUtil.isSvnClient(request)) + { + SvnUtil.sendError(request, response, HttpServletResponse.SC_FORBIDDEN, + ScmSvnErrorCode.AUTHZ_NOT_ENOUGH_PRIVILEGES); + } + else + { + super.sendNotEnoughPrivilegesError(request, response); + } + } + //~--- get methods ---------------------------------------------------------- /** diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java index 689af8851b..9eda6fe7dd 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java @@ -69,7 +69,7 @@ public class SvnServletModule extends ServletModule protected void configureServlets() { filter(PATTERN_SVN).through(SvnGZipFilter.class); - filter(PATTERN_SVN).through(BasicAuthenticationFilter.class); + filter(PATTERN_SVN).through(SvnBasicAuthenticationFilter.class); filter(PATTERN_SVN).through(SvnPermissionFilter.class); Map parameters = new HashMap();