Browse Source

SEC-525: [PATCH] Add AccessCheckerTag based on URL resource access permissions. Added functionality to "authorize" tag to allow evaluation of whether a particual url is accessible to the user. Uses a WebInvocationPrivilegeEvaluator registered in the application context.

Luke Taylor 16 years ago
parent
commit
731402e9f5

+ 14 - 3
config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java

@@ -40,6 +40,7 @@ import org.springframework.security.web.FilterChainProxy;
 import org.springframework.security.web.PortResolverImpl;
 import org.springframework.security.web.access.AccessDeniedHandlerImpl;
 import org.springframework.security.web.access.ExceptionTranslationFilter;
+import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator;
 import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl;
 import org.springframework.security.web.access.channel.ChannelProcessingFilter;
 import org.springframework.security.web.access.channel.InsecureChannelProcessor;
@@ -218,7 +219,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
 
             sessionStrategyRef = (BeanReference) (sessionStrategyPV == null ? null : sessionStrategyPV.getValue());
         }
-        BeanDefinition fsi = createFilterSecurityInterceptor(element, pc, matcher, convertPathsToLowerCase, authenticationManager);
+        BeanReference fsi = createFilterSecurityInterceptor(element, pc, matcher, convertPathsToLowerCase, authenticationManager);
 
         if (channelRequestMap.size() > 0) {
             // At least one channel requirement has been specified
@@ -791,7 +792,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
         return accessDeniedHandler.getBeanDefinition();
     }
 
-    private BeanDefinition createFilterSecurityInterceptor(Element element, ParserContext pc, UrlMatcher matcher,
+    private BeanReference createFilterSecurityInterceptor(Element element, ParserContext pc, UrlMatcher matcher,
             boolean convertPathsToLowerCase, BeanReference authManager) {
         BeanDefinitionBuilder fidsBuilder;
 
@@ -851,7 +852,17 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
         }
 
         builder.addPropertyValue("securityMetadataSource", fidsBuilder.getBeanDefinition());
-        return builder.getBeanDefinition();
+        BeanDefinition fsi = builder.getBeanDefinition();
+        String fsiId = pc.getReaderContext().registerWithGeneratedName(fsi);
+        pc.registerBeanComponent(new BeanComponentDefinition(fsi,fsiId));
+
+        // Create and register a DefaultWebInvocationPrivilegeEvaluator for use with taglibs etc.
+        BeanDefinition wipe = new RootBeanDefinition(DefaultWebInvocationPrivilegeEvaluator.class);
+        wipe.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanReference(fsiId));
+        String wipeId = pc.getReaderContext().registerWithGeneratedName(wipe);
+        pc.registerBeanComponent(new BeanComponentDefinition(wipe, wipeId));
+
+        return new RuntimeBeanReference(fsiId);
     }
 
     private BeanDefinition createChannelProcessingFilter(ParserContext pc, UrlMatcher matcher,

+ 9 - 2
samples/tutorial/src/main/webapp/index.jsp

@@ -11,8 +11,15 @@ If you're logged in, you can <a href="listAccounts.html">list accounts</a>.
 <p>
 Your principal object is....: <%= request.getUserPrincipal() %>
 </p>
+<p>
+<sec:authorize url='/secure/index.jsp'>You can currently access "/secure" URLs.</sec:authorize>
+</p>
+<p>
+<sec:authorize url='/secure/extreme/index.jsp'>You can currently access "/secure/extreme" URLs.</sec:authorize>
+</p>
 
-<p><a href="secure/index.jsp">Secure page</a></p>
+<p>
+<a href="secure/index.jsp">Secure page</a></p>
 <p><a href="secure/extreme/index.jsp">Extremely secure page</a></p>
 </body>
-</html>
+</html>

+ 46 - 7
taglibs/src/main/java/org/springframework/security/taglibs/authz/AuthorizeTag.java

@@ -8,6 +8,7 @@ import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.jsp.JspException;
 
 import org.springframework.context.ApplicationContext;
@@ -17,31 +18,43 @@ import org.springframework.security.access.expression.ExpressionUtils;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.web.FilterInvocation;
+import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
 import org.springframework.security.web.access.expression.WebSecurityExpressionHandler;
 import org.springframework.web.context.support.WebApplicationContextUtils;
 
 /**
- * Expression-based access control tag.
- *
+ * Access control tag which evaluates its body based either on
+ * <ul>
+ * <li>an access expression (the "access" attribute), or</li>
+ * <li>by evaluating the current user's right to access a particular URL (set using the "url" attribute).</li>
+ * </ul>
  * @author Luke Taylor
  * @version $Id$
  * @since 3.0
  */
 public class AuthorizeTag extends LegacyAuthorizeTag {
     private String access;
+    private String url;
+    private String method;
 
     // If access expression evaluates to "true" return
     public int doStartTag() throws JspException {
-        if (access == null || access.length() == 0) {
-            return super.doStartTag();
-        }
-
         Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();
 
         if (currentUser == null) {
             return SKIP_BODY;
         }
 
+        if (access != null && access.length() > 0) {
+            return authorizeUsingAccessExpression(currentUser);
+        } else if (url != null && url.length() > 0) {
+            return authorizeUsingUrlCheck(currentUser);
+        }
+
+        return super.doStartTag();
+    }
+
+    private int authorizeUsingAccessExpression(Authentication currentUser) throws JspException {
         // Get web expression
         WebSecurityExpressionHandler handler = getExpressionHandler();
 
@@ -62,10 +75,23 @@ public class AuthorizeTag extends LegacyAuthorizeTag {
         return SKIP_BODY;
     }
 
+    private int authorizeUsingUrlCheck(Authentication currentUser) throws JspException {
+        return getPrivilegeEvaluator().isAllowed(((HttpServletRequest)pageContext.getRequest()).getContextPath(),
+                url, method, currentUser) ? EVAL_BODY_INCLUDE : SKIP_BODY;
+    }
+
     public void setAccess(String access) {
         this.access = access;
     }
 
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public void setMethod(String method) {
+        this.method = method;
+    }
+
     WebSecurityExpressionHandler getExpressionHandler() throws JspException {
         ServletContext servletContext = pageContext.getServletContext();
         ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
@@ -73,12 +99,25 @@ public class AuthorizeTag extends LegacyAuthorizeTag {
 
         if (expressionHdlrs.size() == 0) {
             throw new JspException("No visible WebSecurityExpressionHandler instance could be found in the application " +
-                    "context. There must be at least one in order to use expressions with taglib support.");
+                    "context. There must be at least one in order to support expressions in JSP 'authorize' tags.");
         }
 
         return (WebSecurityExpressionHandler) expressionHdlrs.values().toArray()[0];
     }
 
+    WebInvocationPrivilegeEvaluator getPrivilegeEvaluator() throws JspException {
+        ServletContext servletContext = pageContext.getServletContext();
+        ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
+        Map<String, WebInvocationPrivilegeEvaluator> wipes = ctx.getBeansOfType(WebInvocationPrivilegeEvaluator.class);
+
+        if (wipes.size() == 0) {
+            throw new JspException("No visible WebInvocationPrivilegeEvaluator instance could be found in the application " +
+                    "context. There must be at least one in order to support the use of URL access checks in 'authorize' tags.");
+        }
+
+        return (WebInvocationPrivilegeEvaluator) wipes.values().toArray()[0];
+    }
+
     private static final FilterChain DUMMY_CHAIN = new FilterChain() {
         public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
            throw new UnsupportedOperationException();

+ 22 - 0
taglibs/src/main/resources/META-INF/security.tld

@@ -30,6 +30,28 @@
             </description>
         </attribute>
 
+        <attribute>
+            <name>url</name>
+            <required>false</required>
+            <rtexprvalue>false</rtexprvalue>
+            <description>
+                A URL within the application. If the user has access to this URL (as determined by
+                the AccessDecisionManager), the tag body will be evaluated. If not, it will
+                be skipped.
+            </description>
+        </attribute>
+
+        <attribute>
+            <name>method</name>
+            <required>false</required>
+            <rtexprvalue>false</rtexprvalue>
+            <description>
+                Can optionally be used to narrow down the HTTP method (typically GET or POST) to which the URL
+                applies to. Only has any meaning when used in combination with the "url" attribute.
+            </description>
+        </attribute>
+
+
         <attribute>
             <name>ifNotGranted</name>
             <required>false</required>

+ 44 - 0
taglibs/src/test/java/org/springframework/security/taglibs/authz/AuthorizeTagTests.java

@@ -28,7 +28,9 @@ import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.mock.web.MockPageContext;
 import org.springframework.mock.web.MockServletContext;
 import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
 import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
 import org.springframework.web.context.WebApplicationContext;
 import org.springframework.web.context.support.StaticWebApplicationContext;
@@ -36,6 +38,7 @@ import org.springframework.web.context.support.StaticWebApplicationContext;
 
 /**
  * @author Francois Beausoleil
+ * @author Luke Taylor
  * @version $Id$
  */
 public class AuthorizeTagTests {
@@ -51,6 +54,7 @@ public class AuthorizeTagTests {
         SecurityContextHolder.getContext().setAuthentication(currentUser);
         StaticWebApplicationContext ctx = new StaticWebApplicationContext();
         ctx.registerSingleton("expressionHandler", DefaultWebSecurityExpressionHandler.class);
+        ctx.registerSingleton("wipe", MockWebInvocationPrivilegeEvaluator.class);
         MockServletContext servletCtx = new MockServletContext();
         servletCtx.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ctx);
         authorizeTag = new AuthorizeTag();
@@ -82,6 +86,35 @@ public class AuthorizeTagTests {
         authorizeTag.setAccess("permitAll");
         assertEquals(Tag.EVAL_BODY_INCLUDE, authorizeTag.doStartTag());
     }
+
+    // url attribute tests
+    @Test
+    public void skipsBodyWithUrlSetIfNoAuthenticationPresent() throws Exception {
+        SecurityContextHolder.clearContext();
+        authorizeTag.setUrl("/something");
+        assertEquals(Tag.SKIP_BODY, authorizeTag.doStartTag());
+    }
+
+    @Test
+    public void skipsBodyIfUrlIsNotAllowed() throws Exception {
+        authorizeTag.setUrl("/notallowed");
+        assertEquals(Tag.SKIP_BODY, authorizeTag.doStartTag());
+    }
+
+    @Test
+    public void evaluatesBodyIfUrlIsAllowed() throws Exception {
+        authorizeTag.setUrl("/allowed");
+        authorizeTag.setMethod("GET");
+        assertEquals(Tag.EVAL_BODY_INCLUDE, authorizeTag.doStartTag());
+    }
+
+    @Test
+    public void skipsBodyIfMethodIsNotAllowed() throws Exception {
+        authorizeTag.setUrl("/allowed");
+        authorizeTag.setMethod("POST");
+        assertEquals(Tag.SKIP_BODY, authorizeTag.doStartTag());
+    }
+
     // Legacy attribute tests
 
     @Test
@@ -144,4 +177,15 @@ public class AuthorizeTagTests {
         authorizeTag.setIfNotGranted("ROLE_TELLER");
         assertEquals("prevents request - principal has ROLE_TELLER", Tag.SKIP_BODY, authorizeTag.doStartTag());
     }
+
+    public static class MockWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator {
+
+        public boolean isAllowed(String uri, Authentication authentication) {
+            return "/allowed".equals(uri);
+        }
+
+        public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) {
+            return "/allowed".equals(uri) && (method == null || "GET".equals(method));
+        }
+    }
 }

+ 1 - 1
web/pom.xml

@@ -28,7 +28,7 @@
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-test</artifactId>
-            <optional>true</optional>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>jaxen</groupId>

+ 536 - 0
web/src/main/java/org/springframework/security/web/access/DefaultWebInvocationPrivilegeEvaluator.java

@@ -0,0 +1,536 @@
+/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.access;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.security.Principal;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.servlet.FilterChain;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.FilterInvocation;
+import org.springframework.util.Assert;
+
+
+/**
+ * Allows users to determine whether they have privileges for a given web URI.
+ *
+ * @author Ben Alex
+ * @author Luke Taylor
+ * @version $Id$
+ * @since 3.0
+ */
+public class DefaultWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator {
+    //~ Static fields/initializers =====================================================================================
+
+    protected static final Log logger = LogFactory.getLog(DefaultWebInvocationPrivilegeEvaluator.class);
+
+    static final FilterChain DUMMY_CHAIN = new FilterChain() {
+        public void doFilter(ServletRequest req, ServletResponse res) throws IOException, ServletException {
+            throw new UnsupportedOperationException("DefaultWebInvocationPrivilegeEvaluator does not support filter chains");
+        }
+    };
+
+    static final HttpServletResponse DUMMY_RESPONSE = new DummyResponse();
+
+    //~ Instance fields ================================================================================================
+
+    private AbstractSecurityInterceptor securityInterceptor;
+
+    //~ Constructors ===================================================================================================
+
+    public DefaultWebInvocationPrivilegeEvaluator(AbstractSecurityInterceptor securityInterceptor) {
+        Assert.notNull(securityInterceptor, "SecurityInterceptor cannot be null");
+        Assert.isTrue(FilterInvocation.class.equals(securityInterceptor.getSecureObjectClass()),
+            "AbstractSecurityInterceptor does not support FilterInvocations");
+        Assert.notNull(securityInterceptor.getAccessDecisionManager(),
+            "AbstractSecurityInterceptor must provide a non-null AccessDecisionManager");
+
+        this.securityInterceptor = securityInterceptor;
+    }
+
+    //~ Methods ========================================================================================================
+
+    /**
+     * Determines whether the user represented by the supplied <tt>Authentication</tt> object is
+     * allowed to invoke the supplied URI.
+     *
+     * @param uri the URI excluding the context path (a default context path setting will be used)
+     */
+    public boolean isAllowed(String uri, Authentication authentication) {
+        return isAllowed(null, uri, null, authentication);
+    }
+
+    /**
+     * Determines whether the user represented by the supplied <tt>Authentication</tt> object is
+     * allowed to invoke the supplied URI, with the given .
+     * <p>
+     * Note the default implementation of <tt>FilterInvocationSecurityMetadataSource</tt> disregards the
+     * <code>contextPath</code> when evaluating which secure object metadata applies to a given
+     * request URI, so generally the <code>contextPath</code> is unimportant unless you
+     * are using a custom <code>FilterInvocationSecurityMetadataSource</code>.
+     *
+     * @param uri the URI excluding the context path
+     * @param contextPath the context path (may be null, in which case a default value will be used).
+     * @param method the HTTP method (or null, for any method)
+     * @param authentication the <tt>Authentication</tt> instance whose authorities should be used in evaluation
+     *          whether access should be granted.
+     * @return true if access is allowed, false if denied
+     */
+    public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) {
+        Assert.notNull(uri, "uri parameter is required");
+
+        if (contextPath == null) {
+            contextPath = "/ctxpath";
+        }
+
+        FilterInvocation fi = createFilterInvocation(contextPath, uri, method);
+        List<ConfigAttribute> attrs = securityInterceptor.obtainSecurityMetadataSource().getAttributes(fi);
+
+        if (attrs == null) {
+            if (securityInterceptor.isRejectPublicInvocations()) {
+                return false;
+            }
+
+            return true;
+        }
+
+        if ((authentication == null) || (authentication.getAuthorities() == null)
+                || authentication.getAuthorities().isEmpty()) {
+            return false;
+        }
+
+        try {
+            securityInterceptor.getAccessDecisionManager().decide(authentication, fi, attrs);
+        } catch (AccessDeniedException unauthorized) {
+            if (logger.isDebugEnabled()) {
+                logger.debug(fi.toString() + " denied for " + authentication.toString(), unauthorized);
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    private FilterInvocation createFilterInvocation(String contextPath, String uri, String method) {
+        Assert.hasText(contextPath, "contextPath required");
+        Assert.hasText(uri, "URI required");
+
+        DummyRequest req = new DummyRequest();
+        req.setRequestURI(contextPath + uri);
+        req.setContextPath(contextPath);
+        req.setServletPath(null);
+        req.setMethod(method);
+
+        return new FilterInvocation(req, DUMMY_RESPONSE, DUMMY_CHAIN);
+    }
+}
+
+@SuppressWarnings("unchecked")
+class DummyRequest implements HttpServletRequest {
+    private String requestURI;
+    private String contextPath = "";
+    private String servletPath;
+    private String method;
+
+    public void setRequestURI(String requestURI) {
+        this.requestURI = requestURI;
+    }
+
+    public String getRequestURI() {
+        return requestURI;
+    }
+
+    public void setContextPath(String contextPath) {
+        this.contextPath = contextPath;
+    }
+
+    public String getContextPath() {
+        return contextPath;
+    }
+
+    public void setServletPath(String servletPath) {
+        this.servletPath = servletPath;
+    }
+
+    public String getServletPath() {
+        return servletPath;
+    }
+
+    public void setMethod(String method) {
+        this.method = method;
+    }
+
+    public String getMethod() {
+        return method;
+    }
+
+    public String getPathInfo() {
+        return null;
+    }
+
+    public String getQueryString() {
+        return null;
+    }
+
+    public String getAuthType() {
+        throw new UnsupportedOperationException();
+    }
+
+    public Cookie[] getCookies() {
+        throw new UnsupportedOperationException();
+    }
+
+    public long getDateHeader(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getHeader(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Enumeration getHeaderNames() {
+        throw new UnsupportedOperationException();
+    }
+
+    public Enumeration getHeaders(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public int getIntHeader(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getPathTranslated() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getRemoteUser() {
+        throw new UnsupportedOperationException();
+    }
+
+    public StringBuffer getRequestURL() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getRequestedSessionId() {
+        throw new UnsupportedOperationException();
+    }
+
+    public HttpSession getSession() {
+        throw new UnsupportedOperationException();
+    }
+
+    public HttpSession getSession(boolean create) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Principal getUserPrincipal() {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean isRequestedSessionIdFromCookie() {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean isRequestedSessionIdFromURL() {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean isRequestedSessionIdFromUrl() {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean isRequestedSessionIdValid() {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean isUserInRole(String role) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Object getAttribute(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Enumeration getAttributeNames() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getCharacterEncoding() {
+        throw new UnsupportedOperationException();
+    }
+
+    public int getContentLength() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getContentType() {
+        throw new UnsupportedOperationException();
+    }
+
+    public ServletInputStream getInputStream() throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getLocalAddr() {
+        throw new UnsupportedOperationException();
+
+    }
+
+    public String getLocalName() {
+        throw new UnsupportedOperationException();
+    }
+
+    public int getLocalPort() {
+        throw new UnsupportedOperationException();
+    }
+
+    public Locale getLocale() {
+        throw new UnsupportedOperationException();
+    }
+
+    public Enumeration getLocales() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getParameter(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Map getParameterMap() {
+        throw new UnsupportedOperationException();
+    }
+
+    public Enumeration getParameterNames() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String[] getParameterValues(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getProtocol() {
+        throw new UnsupportedOperationException();
+    }
+
+    public BufferedReader getReader() throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getRealPath(String path) {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getRemoteAddr() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getRemoteHost() {
+        throw new UnsupportedOperationException();
+    }
+
+    public int getRemotePort() {
+        throw new UnsupportedOperationException();
+    }
+
+    public RequestDispatcher getRequestDispatcher(String path) {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getScheme() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getServerName() {
+        throw new UnsupportedOperationException();
+    }
+
+    public int getServerPort() {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean isSecure() {
+        throw new UnsupportedOperationException();
+    }
+
+    public void removeAttribute(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setAttribute(String name, Object o) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setCharacterEncoding(String env) throws UnsupportedEncodingException {
+        throw new UnsupportedOperationException();
+    }
+}
+
+class DummyResponse implements HttpServletResponse {
+    public void addCookie(Cookie cookie) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void addDateHeader(String name, long date) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void addHeader(String name, String value) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void addIntHeader(String name, int value) {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean containsHeader(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public String encodeRedirectURL(String url) {
+        throw new UnsupportedOperationException();
+    }
+
+    public String encodeRedirectUrl(String url) {
+        throw new UnsupportedOperationException();
+    }
+
+    public String encodeURL(String url) {
+        throw new UnsupportedOperationException();
+    }
+
+    public String encodeUrl(String url) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void sendError(int sc) throws IOException {
+        throw new UnsupportedOperationException();
+
+    }
+
+    public void sendError(int sc, String msg) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void sendRedirect(String location) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setDateHeader(String name, long date) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setHeader(String name, String value) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setIntHeader(String name, int value) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setStatus(int sc) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setStatus(int sc, String sm) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void flushBuffer() throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    public int getBufferSize() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getCharacterEncoding() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getContentType() {
+        throw new UnsupportedOperationException();
+    }
+
+    public Locale getLocale() {
+        throw new UnsupportedOperationException();
+    }
+
+    public ServletOutputStream getOutputStream() throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    public PrintWriter getWriter() throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean isCommitted() {
+        throw new UnsupportedOperationException();
+    }
+
+    public void reset() {
+        throw new UnsupportedOperationException();
+    }
+
+    public void resetBuffer() {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setBufferSize(int size) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setCharacterEncoding(String charset) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setContentLength(int len) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setContentType(String type) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setLocale(Locale loc) {
+        throw new UnsupportedOperationException();
+    }
+}
+

+ 6 - 99
web/src/main/java/org/springframework/security/web/access/WebInvocationPrivilegeEvaluator.java

@@ -15,60 +15,16 @@
 
 package org.springframework.security.web.access;
 
-import java.io.IOException;
-import java.util.List;
-
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.mock.web.MockHttpServletResponse;
-import org.springframework.security.access.AccessDeniedException;
-import org.springframework.security.access.ConfigAttribute;
-import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
 import org.springframework.security.core.Authentication;
-import org.springframework.security.web.FilterInvocation;
-import org.springframework.util.Assert;
 
 
 /**
  * Allows users to determine whether they have privileges for a given web URI.
  *
- * @author Ben Alex
- * @version $Id$
+ * @author Luke Taylor
+ * @since 3.0
  */
-public class WebInvocationPrivilegeEvaluator {
-    //~ Static fields/initializers =====================================================================================
-
-    protected static final Log logger = LogFactory.getLog(WebInvocationPrivilegeEvaluator.class);
-
-    static final FilterChain DUMMY_CHAIN = new FilterChain() {
-        public void doFilter(ServletRequest req, ServletResponse res) throws IOException, ServletException {
-            throw new UnsupportedOperationException("WebInvocationPrivilegeEvaluator does not support filter chains");
-        }
-    };
-
-    //~ Instance fields ================================================================================================
-
-    private AbstractSecurityInterceptor securityInterceptor;
-
-    //~ Constructors ===================================================================================================
-
-    public WebInvocationPrivilegeEvaluator(AbstractSecurityInterceptor securityInterceptor) {
-        Assert.notNull(securityInterceptor, "SecurityInterceptor cannot be null");
-        Assert.isTrue(FilterInvocation.class.equals(securityInterceptor.getSecureObjectClass()),
-            "AbstractSecurityInterceptor does not support FilterInvocations");
-        Assert.notNull(securityInterceptor.getAccessDecisionManager(),
-            "AbstractSecurityInterceptor must provide a non-null AccessDecisionManager");
-
-        this.securityInterceptor = securityInterceptor;
-    }
-
-    //~ Methods ========================================================================================================
+public interface WebInvocationPrivilegeEvaluator {
 
     /**
      * Determines whether the user represented by the supplied <tt>Authentication</tt> object is
@@ -76,9 +32,7 @@ public class WebInvocationPrivilegeEvaluator {
      *
      * @param uri the URI excluding the context path (a default context path setting will be used)
      */
-    public boolean isAllowed(String uri, Authentication authentication) {
-        return isAllowed(null, uri, null, authentication);
-    }
+    public boolean isAllowed(String uri, Authentication authentication);
 
     /**
      * Determines whether the user represented by the supplied <tt>Authentication</tt> object is
@@ -90,58 +44,11 @@ public class WebInvocationPrivilegeEvaluator {
      * are using a custom <code>FilterInvocationSecurityMetadataSource</code>.
      *
      * @param uri the URI excluding the context path
-     * @param contextPath the context path (may be null, in which case a default value will be used).
+     * @param contextPath the context path (may be null).
      * @param method the HTTP method (or null, for any method)
      * @param authentication the <tt>Authentication</tt> instance whose authorities should be used in evaluation
      *          whether access should be granted.
      * @return true if access is allowed, false if denied
      */
-    public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) {
-        Assert.notNull(uri, "uri parameter is required");
-
-        if (contextPath == null) {
-            contextPath = "/ctxpath";
-        }
-
-        FilterInvocation fi = createFilterInvocation(contextPath, uri, method);
-        List<ConfigAttribute> attrs = securityInterceptor.obtainSecurityMetadataSource().getAttributes(fi);
-
-        if (attrs == null) {
-            if (securityInterceptor.isRejectPublicInvocations()) {
-                return false;
-            }
-
-            return true;
-        }
-
-        if ((authentication == null) || (authentication.getAuthorities() == null)
-                || authentication.getAuthorities().isEmpty()) {
-            return false;
-        }
-
-        try {
-            securityInterceptor.getAccessDecisionManager().decide(authentication, fi, attrs);
-        } catch (AccessDeniedException unauthorized) {
-            if (logger.isDebugEnabled()) {
-                logger.debug(fi.toString() + " denied for " + authentication.toString(), unauthorized);
-            }
-
-            return false;
-        }
-
-        return true;
-    }
-
-    private FilterInvocation createFilterInvocation(String contextPath, String uri, String method) {
-        Assert.hasText(contextPath, "contextPath required");
-        Assert.hasText(uri, "URI required");
-
-        MockHttpServletRequest req = new MockHttpServletRequest();
-        req.setRequestURI(contextPath + uri);
-        req.setContextPath(contextPath);
-        req.setServletPath(null);
-        req.setMethod(method);
-
-        return new FilterInvocation(req, new MockHttpServletResponse(), DUMMY_CHAIN);
-    }
+    public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication);
 }

+ 24 - 1
web/src/main/java/org/springframework/security/web/util/UrlUtils.java

@@ -70,13 +70,36 @@ public final class UrlUtils {
 
     /**
      * Obtains the web application-specific fragment of the URL.
+     * <p>
+     * Under normal spec conditions,
+     * <pre>
+     * requestURI = contextPath + servletPath + pathInfo
+     * </pre>
+     *
+     * But this method may also be called using dummy request objects which just have the requestURI and contextPath
+     * set, for example.
      *
      * @return the URL, excluding any server name, context path or servlet path
      */
     public static String buildRequestUrl(String servletPath, String requestURI, String contextPath, String pathInfo,
         String queryString) {
 
-        return servletPath + ((pathInfo == null) ? "" : pathInfo) + ((queryString == null) ? "" : ("?" + queryString));
+        StringBuilder url = new StringBuilder();
+
+        if (servletPath != null) {
+            url.append(servletPath);
+            if (pathInfo != null) {
+                url.append(pathInfo);
+            }
+        } else {
+            url.append(requestURI.substring(contextPath.length()));
+        }
+
+        if (queryString != null) {
+            url.append("?").append(queryString);
+        }
+
+        return url.toString();
     }
 
     /**

+ 17 - 8
web/src/test/java/org/springframework/security/web/access/WebInvocationPrivilegeEvaluatorTests.java → web/src/test/java/org/springframework/security/web/access/DefaultWebInvocationPrivilegeEvaluatorTests.java

@@ -34,15 +34,16 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
+import org.springframework.security.web.util.UrlUtils;
 
 
 /**
- * Tests {@link org.springframework.security.web.access.WebInvocationPrivilegeEvaluator}.
+ * Tests {@link org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator}.
  *
  * @author Ben Alex
  * @version $Id$
  */
-public class WebInvocationPrivilegeEvaluatorTests {
+public class DefaultWebInvocationPrivilegeEvaluatorTests {
     private AccessDecisionManager adm;
     private FilterInvocationSecurityMetadataSource ods;
     private RunAsManager ram;
@@ -66,14 +67,14 @@ public class WebInvocationPrivilegeEvaluatorTests {
 
     @Test
     public void permitsAccessIfNoMatchingAttributesAndPublicInvocationsAllowed() throws Exception {
-        WebInvocationPrivilegeEvaluator wipe = new WebInvocationPrivilegeEvaluator(interceptor);
+        DefaultWebInvocationPrivilegeEvaluator wipe = new DefaultWebInvocationPrivilegeEvaluator(interceptor);
         when(ods.getAttributes(anyObject())).thenReturn(null);
         assertTrue(wipe.isAllowed("/context", "/foo/index.jsp", "GET", mock(Authentication.class)));
     }
 
     @Test
     public void deniesAccessIfNoMatchingAttributesAndPublicInvocationsNotAllowed() throws Exception {
-        WebInvocationPrivilegeEvaluator wipe = new WebInvocationPrivilegeEvaluator(interceptor);
+        DefaultWebInvocationPrivilegeEvaluator wipe = new DefaultWebInvocationPrivilegeEvaluator(interceptor);
         when(ods.getAttributes(anyObject())).thenReturn(null);
         interceptor.setRejectPublicInvocations(true);
         assertFalse(wipe.isAllowed("/context", "/foo/index.jsp", "GET", mock(Authentication.class)));
@@ -81,14 +82,14 @@ public class WebInvocationPrivilegeEvaluatorTests {
 
     @Test
     public void deniesAccessIfAuthenticationIsNull() throws Exception {
-        WebInvocationPrivilegeEvaluator wipe = new WebInvocationPrivilegeEvaluator(interceptor);
+        DefaultWebInvocationPrivilegeEvaluator wipe = new DefaultWebInvocationPrivilegeEvaluator(interceptor);
         assertFalse(wipe.isAllowed("/foo/index.jsp", null));
     }
 
     @Test
     public void allowsAccessIfAccessDecisionMangerDoes() throws Exception {
         Authentication token = new TestingAuthenticationToken("test", "Password", "MOCK_INDEX");
-        WebInvocationPrivilegeEvaluator wipe = new WebInvocationPrivilegeEvaluator(interceptor);
+        DefaultWebInvocationPrivilegeEvaluator wipe = new DefaultWebInvocationPrivilegeEvaluator(interceptor);
         assertTrue(wipe.isAllowed("/foo/index.jsp", token));
     }
 
@@ -96,7 +97,7 @@ public class WebInvocationPrivilegeEvaluatorTests {
     @Test
     public void deniesAccessIfAccessDecisionMangerDoes() throws Exception {
         Authentication token = new TestingAuthenticationToken("test", "Password", "MOCK_INDEX");
-        WebInvocationPrivilegeEvaluator wipe = new WebInvocationPrivilegeEvaluator(interceptor);
+        DefaultWebInvocationPrivilegeEvaluator wipe = new DefaultWebInvocationPrivilegeEvaluator(interceptor);
 
         doThrow(new AccessDeniedException("")).when(adm).decide(any(Authentication.class), anyObject(), anyList());
 
@@ -105,6 +106,14 @@ public class WebInvocationPrivilegeEvaluatorTests {
 
     @Test(expected=UnsupportedOperationException.class)
     public void dummyChainRejectsInvocation() throws Exception {
-        WebInvocationPrivilegeEvaluator.DUMMY_CHAIN.doFilter(mock(HttpServletRequest.class), mock(HttpServletResponse.class));
+        DefaultWebInvocationPrivilegeEvaluator.DUMMY_CHAIN.doFilter(mock(HttpServletRequest.class), mock(HttpServletResponse.class));
+    }
+
+    @Test
+    public void dummyRequestIsSupportedByUrlUtils() throws Exception {
+        DummyRequest request = new DummyRequest();
+        request.setContextPath("");
+        request.setRequestURI("/something");
+        UrlUtils.buildRequestUrl(request);
     }
 }