From 731402e9f57ded06af0ce00a4a158da5ea5a05b2 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Wed, 16 Sep 2009 00:23:13 +0000 Subject: [PATCH] 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. --- .../HttpSecurityBeanDefinitionParser.java | 17 +- samples/tutorial/src/main/webapp/index.jsp | 11 +- .../security/taglibs/authz/AuthorizeTag.java | 53 +- .../src/main/resources/META-INF/security.tld | 22 + .../taglibs/authz/AuthorizeTagTests.java | 44 ++ web/pom.xml | 2 +- ...efaultWebInvocationPrivilegeEvaluator.java | 536 ++++++++++++++++++ .../WebInvocationPrivilegeEvaluator.java | 105 +--- .../security/web/util/UrlUtils.java | 25 +- ...WebInvocationPrivilegeEvaluatorTests.java} | 25 +- 10 files changed, 719 insertions(+), 121 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/access/DefaultWebInvocationPrivilegeEvaluator.java rename web/src/test/java/org/springframework/security/web/access/{WebInvocationPrivilegeEvaluatorTests.java => DefaultWebInvocationPrivilegeEvaluatorTests.java} (78%) diff --git a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java index e0d7a49033..f8fafb1656 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java +++ b/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, diff --git a/samples/tutorial/src/main/webapp/index.jsp b/samples/tutorial/src/main/webapp/index.jsp index edf1d00d0b..bd1690e57c 100644 --- a/samples/tutorial/src/main/webapp/index.jsp +++ b/samples/tutorial/src/main/webapp/index.jsp @@ -11,8 +11,15 @@ If you're logged in, you can list accounts.

Your principal object is....: <%= request.getUserPrincipal() %>

+

+You can currently access "/secure" URLs. +

+

+You can currently access "/secure/extreme" URLs. +

-

Secure page

+

+Secure page

Extremely secure page

- \ No newline at end of file + diff --git a/taglibs/src/main/java/org/springframework/security/taglibs/authz/AuthorizeTag.java b/taglibs/src/main/java/org/springframework/security/taglibs/authz/AuthorizeTag.java index 1caf3e5d24..23cca4684d 100644 --- a/taglibs/src/main/java/org/springframework/security/taglibs/authz/AuthorizeTag.java +++ b/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 + * * @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 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(); diff --git a/taglibs/src/main/resources/META-INF/security.tld b/taglibs/src/main/resources/META-INF/security.tld index d4683d7e0a..90bf262b79 100644 --- a/taglibs/src/main/resources/META-INF/security.tld +++ b/taglibs/src/main/resources/META-INF/security.tld @@ -30,6 +30,28 @@ + + url + false + false + + 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. + + + + + method + false + false + + 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. + + + + ifNotGranted false diff --git a/taglibs/src/test/java/org/springframework/security/taglibs/authz/AuthorizeTagTests.java b/taglibs/src/test/java/org/springframework/security/taglibs/authz/AuthorizeTagTests.java index 755c54b002..d022fb7464 100644 --- a/taglibs/src/test/java/org/springframework/security/taglibs/authz/AuthorizeTagTests.java +++ b/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)); + } + } } diff --git a/web/pom.xml b/web/pom.xml index 768a4bba43..48c45189b1 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -28,7 +28,7 @@ org.springframework spring-test - true + test jaxen diff --git a/web/src/main/java/org/springframework/security/web/access/DefaultWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/DefaultWebInvocationPrivilegeEvaluator.java new file mode 100644 index 0000000000..3f8036550a --- /dev/null +++ b/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 Authentication 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 Authentication object is + * allowed to invoke the supplied URI, with the given . + *

+ * Note the default implementation of FilterInvocationSecurityMetadataSource disregards the + * contextPath when evaluating which secure object metadata applies to a given + * request URI, so generally the contextPath is unimportant unless you + * are using a custom FilterInvocationSecurityMetadataSource. + * + * @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 Authentication 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 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(); + } +} + diff --git a/web/src/main/java/org/springframework/security/web/access/WebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/WebInvocationPrivilegeEvaluator.java index 3aaafc4c11..6d3c57bf0b 100644 --- a/web/src/main/java/org/springframework/security/web/access/WebInvocationPrivilegeEvaluator.java +++ b/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 Authentication 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 Authentication object is @@ -90,58 +44,11 @@ public class WebInvocationPrivilegeEvaluator { * are using a custom FilterInvocationSecurityMetadataSource. * * @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 Authentication 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 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); } diff --git a/web/src/main/java/org/springframework/security/web/util/UrlUtils.java b/web/src/main/java/org/springframework/security/web/util/UrlUtils.java index 65e9ede82b..a4ae7427a9 100644 --- a/web/src/main/java/org/springframework/security/web/util/UrlUtils.java +++ b/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. + *

+ * Under normal spec conditions, + *

+     * requestURI = contextPath + servletPath + pathInfo
+     * 
+ * + * 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(); } /** diff --git a/web/src/test/java/org/springframework/security/web/access/WebInvocationPrivilegeEvaluatorTests.java b/web/src/test/java/org/springframework/security/web/access/DefaultWebInvocationPrivilegeEvaluatorTests.java similarity index 78% rename from web/src/test/java/org/springframework/security/web/access/WebInvocationPrivilegeEvaluatorTests.java rename to web/src/test/java/org/springframework/security/web/access/DefaultWebInvocationPrivilegeEvaluatorTests.java index 3bd44e8e3a..1d91007fc1 100644 --- a/web/src/test/java/org/springframework/security/web/access/WebInvocationPrivilegeEvaluatorTests.java +++ b/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); } }