Commits

Sebastian Sdorra committed 9e992b8

prompt authentication again for failed subversion authentication and improved error message for missing privileges

Comments (0)

Files changed (6)

scm-core/src/main/java/sonia/scm/util/HttpUtil.java

 
 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;
 //~--- JDK imports ------------------------------------------------------------
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
 
 import java.net.URLDecoder;
   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);
 
    */
   private static final Pattern PATTERN_URLNORMALIZE =
     Pattern.compile("(?:(http://[^:]+):80(/.+)?|(https://[^:]+):443(/.+)?)");
-  
+
   /**
    * CharMatcher to select cr/lf and '%' characters
    * @since 1.28
    */
   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);
   }
 
   /**
+   * 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
    *
    *
    * @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);
    * @since 1.19
    */
   public static void sendUnauthorized(HttpServletRequest request,
-    HttpServletResponse response,
-    String realmDescription)
+    HttpServletResponse response, String realmDescription)
     throws IOException
   {
     if ((request == null) ||!isWUIRequest(request))
   }
 
   /**
+   * 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.
    *
    *

scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/ScmSvnErrorCode.java

+/**
+ * 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);
+  }
+}

scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnUtil.java

 
 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
 {
 
   /** 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:";
 
   /**
    */
   private static final char TYPE_UPDATED = 'U';
 
+  /** Field description */
+  private static final String USERAGENT_SVN = "svn/";
+
   /**
    * the logger for SvnUtil
    */
   /**
    * Method description
    *
+   * @param errorCode
+   *
+   * @return
+   */
+  public static String createErrorBody(SVNErrorCode errorCode)
+  {
+    StringBuffer xmlBuffer = new StringBuffer();
+
+    SVNXMLUtil.addXMLHeader(xmlBuffer);
+
+    List<String> 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
+   *
    *
    * @param transaction
    *
     }
   }
 
+  /**
+   * 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 ----------------------------------------------------------
 
   /**
    * 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
+   *
+   *
    * @param id
    *
    * @return

scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java

+/**
+ * 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<AutoLoginModule> 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);
+    }
+  }
+}

scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java

 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;
 
 /**
  *
 {
 
   /** Field description */
-  private static Set<String> WRITEMETHOD_SET = ImmutableSet.of("MKACTIVITY",
-                                                 "PROPPATCH", "PUT",
-                                                 "CHECKOUT", "MKCOL", "MOVE",
-                                                 "COPY", "DELETE", "LOCK",
-                                                 "UNLOCK", "MERGE");
+  private static final Set<String> WRITEMETHOD_SET =
+    ImmutableSet.of("MKACTIVITY", "PROPPATCH", "PUT", "CHECKOUT", "MKCOL",
+      "MOVE", "COPY", "DELETE", "LOCK", "UNLOCK", "MERGE");
 
   //~--- constructors ---------------------------------------------------------
 
   /**
    * Constructs ...
    *
-   *
-   *
-   *
    * @param configuration
-   * @param securityContextProvider
    * @param repository
    */
   @Inject
     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 ----------------------------------------------------------
 
   /**

scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java

   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<String, String> parameters = new HashMap<String, String>();