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.

This commit is contained in:
Luke Taylor 2009-09-16 00:23:13 +00:00
parent 1c4a809e09
commit 731402e9f5
10 changed files with 719 additions and 121 deletions

View File

@ -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,

View File

@ -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>

View File

@ -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();

View File

@ -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>

View File

@ -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));
}
}
}

View File

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

View File

@ -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();
}
}

View File

@ -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);
}

View File

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

View File

@ -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);
}
}