1. Securing Tapestry 5 pages with Spring Security 2.x (Part I)

    Securing wooki was a big dilemma. There was a long discussion on Tapestry 5 user mailing list on how to implement security in a Tapestry 5 application, also Howard Lewis Ship (Creator of Tapestry) has written this really usefull article on his blog http://tapestryjava.blogspot.com/2009/12/securing-tapestry-pages-with.html. On the other side spring security seems to provide a full pipeline of services focused on all the security aspects from authentication, to authorization, with an extensible mechanism of filtering.

    I have made the choice of Spring security despite its apparent complexity for many reasons, some of them are :

    • We have decided to consider security as a layer that should be executed as soon as possible in the request pipeline
    • For the future of Wooki, we want to integrate other technologies like scala, rest api… and security will stay in front of it
    • Also, this time i wanted to experiment an integration of Tapestry into another technology rather than the inverse :)

    For people who wants to have an other approach, it should be interesting to have a look at this existing Tapestry 5 spring security contribution. By the way, now the choice has been done, i had to implement it.

    Security policy

    I wanted to implement a ‘Trust no one’ strategy. This implies to declare all public URLs and apply security rules on all the remaining URL.

    As a reminder, Tapestry URLs are of two types :

    • Render URLs : <package>/<pageName>/<activationCtx>
    • Action URLs : <package>/<pageName>.<componentId>:<eventType>/<eventCtx>?t:ac=<activationCtx>

    URL can also contain extra informations like containing page or client state for @Persist(“client”) datas.

    Spring security is basically based on applying patterns to URLs, then once a pattern has matched, spring security applies a list of filter on it. Pattern expressions can be written in an Ant format or via Regexp.

    On the other side, Tapestry provides a URL optimization mechanism to shorten URL when we access to the index page. For exemple this URL ‘/book/index/3′ to read book number 3 can be written ‘/book/3′, and Tapestry will match automatically to the index page with ’3′ as the activation context.

    So, if we apply our ‘trust no one’ policy,  first we have to declare all Tapestry public pages. Different solutions exists for that, i should have written multiple regexp pattern to match the different URL pattern of my application, but this implies to know all the public URL formats of the application and follow their evolution. Also, doing this is a bit limitative for Tapestry URLs which contains a valuable of information on the targeted resource.

    I have preferred the following approach : Spring security allows us to develop our own UrlMatcher class, so i decided to implement my own that will allow to match all tapestry public pages in a single place without this multiple pattern consideration. My Spring security is basically configured like this :

    1. Declare all public URLs that will not evolve in the application and match them to the corresponding list of filters
    2. Match a special pattern “tapestry-public-pages” on a basic list of filter
    3. For all the remaining URL, we apply our security rules

    This solution has the advantage of delegating some pattern treatments to a custom URL Matcher that will provide all the features to analyze the request for the targeted resource.

    Implement a Delegating UrlMatcher

    The implementation suggested below allows to map special pattern to a different UrlMatcher object than the default (a RegexUrlMatcher in our case)

    
    public class DelegatingUrlPathMatcher implements UrlMatcher {
    
    	private RegexUrlPathMatcher defaultMatcher;
    
    	private Map<String, WookiPathMatcher> matchers = new HashMap<String, WookiPathMatcher>();
    
    	private boolean requiresLowerCaseUrl;
    
    	public DelegatingUrlPathMatcher(Map<String, WookiPathMatcher> matchers) {
    		this.defaultMatcher = new RegexUrlPathMatcher();
    		if (matchers != null) {
    			this.matchers.putAll(matchers);
    		}
    	}
    
    	public Object compile(String urlPattern) {
    		if (matchers.containsKey(urlPattern)) {
    			return matchers.get(urlPattern);
    		}
    		return this.defaultMatcher.compile(urlPattern);
    	}
    
    	public String getUniversalMatchPattern() {
    		return this.defaultMatcher.getUniversalMatchPattern();
    	}
    
    	public boolean pathMatchesUrl(Object compiledUrlPattern, String url) {
    		if (compiledUrlPattern instanceof Pattern) {
    			Pattern pattern = (Pattern) compiledUrlPattern;
    			return pattern.matcher(url).matches();
    		}
    		WookiPathMatcher matcher = (WookiPathMatcher) compiledUrlPattern;
    		return matcher.matches(url);
    	}
    
    	public boolean requiresLowerCaseUrl() {
    		return this.requiresLowerCaseUrl;
    	}
    
    	public void setRequiresLowerCaseUrl(boolean requiresLowerCaseUrl) {
    		this.defaultMatcher.setRequiresLowerCaseUrl(requiresLowerCaseUrl);
    		this.requiresLowerCaseUrl = requiresLowerCaseUrl;
    	}
    
    }
    
    

    Now we have the delegating UrlMatcher, we can develop the one that will be dedicated to match Tapestry public pages. The code below corresponds to the abstract class that will allow us to implement multiple concrete Tapestry URL matchers. Note that it’s important that this class implements the Spring ServletContextAware interface so we can have access to the Tapestry registry through the servlet context. In fact, there is a great feature in Tapestry that allows us to have access to all the services provided by it, and in our case this is the service that analyzes URLs. I won’t go into detailed explanation, but code builds a MockHttpRequest from a String path and called the tapestry services to have access to all the Tapestry parameters.

    public abstract class AbstractTapestryUrlPathMatcher implements WookiPathMatcher, ServletContextAware {
    
    	private Registry tapestryRegistry;
    
    	private RequestGlobals globals;
    
    	private ComponentEventLinkEncoder encoder;
    
    	private String applicationCharset;
    
    	private SessionPersistedObjectAnalyzer spoa;
    
    	private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
    	private String encoding;
    
    	/**
    	 * Initialize Tapestry registry.
    	 *
    	 */
    	public void setServletContext(ServletContext servletContext) {
    		this.tapestryRegistry = (Registry) servletContext.getAttribute(TapestryFilter.REGISTRY_CONTEXT_NAME);
    		this.encoder = this.tapestryRegistry.getService(ComponentEventLinkEncoder.class);
    		this.spoa = this.tapestryRegistry.getService(SessionPersistedObjectAnalyzer.class);
    		this.applicationCharset = this.tapestryRegistry.getService(SymbolSource.class).valueForSymbol(SymbolConstants.CHARSET);
    		this.globals = this.tapestryRegistry.getService(RequestGlobals.class);
    	}
    
    	/**
    	 * Hide Tapestry logic for analyzing URLs
    	 *
    	 * @param request
    	 * @return
    	 */
    	protected ComponentEventRequestParameters decodeComponentEventRequest(String path) {
    		RequestImpl tapRequest = new RequestImpl(this.createRequestForTapestry(path), applicationCharset, spoa);
    		globals.storeRequestResponse(tapRequest, null);
    		return this.encoder.decodeComponentEventRequest(tapRequest);
    	}
    
    	/**
    	 * Hide Tapestry logic for analyzing URLs
    	 *
    	 * @param request
    	 * @return
    	 */
    	protected PageRenderRequestParameters decodePageRenderRequest(String path) {
    		RequestImpl tapRequest = new RequestImpl(this.createRequestForTapestry(path), applicationCharset, spoa);
    		globals.storeRequestResponse(tapRequest, null);
    		return this.encoder.decodePageRenderRequest(tapRequest);
    	}
    
    	/**
    	 * Create a mock http request from a path.
    	 *
    	 * @param path
    	 * @return
    	 */
    	private HttpServletRequest createRequestForTapestry(String path) {
    		MockHttpServletRequest request = new MockHttpServletRequest();
    		int queryIndex = path.indexOf("?");
    		if (queryIndex > -1) {
    			request.setPathInfo(cleanupPath(path.substring(0, queryIndex)));
    			String query = path.substring(path.indexOf("?") + 1);
    			String[] parameters = query.split("&");
    			if (parameters != null) {
    				for (String parameter : parameters) {
    					String[] keyValue = parameter.split("=");
    					if (keyValue != null && keyValue.length == 2) {
    						try {
    							request.addParameter(keyValue[0], URLDecoder.decode(keyValue[1], this.encoding));
    						} catch (UnsupportedEncodingException e) {
    							logger.error("Cannot decode URL parameter with " + this.encoding + " encoding");
    							request.addParameter(keyValue[0], URLDecoder.decode(keyValue[1]));
    						}
    					}
    				}
    			}
    		} else {
    			request.setPathInfo(cleanupPath(path));
    		}
    		return request;
    	}
    
    	/**
    	 * Simply remove extra character added by servlet container for JSession id.
    	 *
    	 * @param path
    	 * @return
    	 */
    	private String cleanupPath(String path) {
    		if (path.contains(";")) {
    			return path.substring(0, path.indexOf(";"));
    		}
    		return path;
    	}
    
    	public String getEncoding() {
    		return encoding;
    	}
    
    	public void setEncoding(String encoding) {
    		this.encoding = encoding;
    	}
    
    }
    

    Now we have the base class, we can implement our UrlMatcher that will match all the Tapestry public pages. As you can see, the code is pretty easy since the complexity of analyzing Tapestry URL has been hidden in the abstract class.

    public class TapestryPublicUrlPathMatcher extends AbstractTapestryUrlPathMatcher {
    
    	/**
    	 * List of public pages.
    	 */
    	private List<String> publicPages = new ArrayList<String>();
    
    	/**
    	 * All the public pages are
    	 *
    	 * @param publicPages
    	 */
    	public TapestryPublicUrlPathMatcher(List<String> publicPages) {
    		if (publicPages != null) {
    			for (String pageName : publicPages) {
    				this.publicPages.add(pageName.toLowerCase());
    			}
    		}
    	}
    
    	/**
    	 * This method tries to find a declared public in tapestry.
    	 *
    	 */
    	public boolean matches(String url) {
    
    		// Secure Render request
    		PageRenderRequestParameters params = this.decodePageRenderRequest(url);
    		if (params != null) {
    			String logicalPageName = params.getLogicalPageName();
    			if (this.publicPages.contains(logicalPageName.toLowerCase())) {
    				return true;
    			}
    		}
    
    		// Secure actions request
    		ComponentEventRequestParameters actionParams = this.decodeComponentEventRequest(url);
    		if (actionParams != null) {
    			String logicalPageName = actionParams.getContainingPageName();
    			if (this.publicPages.contains(logicalPageName.toLowerCase())) {
    				return true;
    			}
    		}
    
    		return false;
    
    	}
    
    }
    

    Configuring spring security

    Now we have all the elements in hands, we only have to configure spring security to use our custom implementation of UrlMatcher.

    Declare a bean that will maintain the list of all tapestry public pages

    
    	<bean id="tapestryPublicPageMatcher"
    		class="com.wooki.services.security.spring.TapestryPublicUrlPathMatcher">
    		<constructor-arg>
    			<list>
    				<value></value>
    				<value>index</value>
    				<value>book</value>
    				<value>book/index</value>
    				<value>chapter</value>
    				<value>chapter/index</value>
    				...
    			</list>
    		</constructor-arg>
    		<property name="encoding" value="${tapestry.charset}" />
    	</bean>
    
    

    Declare our delegating URL Matcher and configure it to use our TapestryPublicUrlMatcher when the pattern is equal to “tapestry-public-page”

          <bean id="delegatingUrlMatcher"
    		class="com.wooki.services.security.spring.DelegatingUrlPathMatcher">
    		<constructor-arg>
    			<map>
    				<entry key="tapestry-public-page" value-ref="tapestryPublicPageMatcher" />
    			</map>
    		</constructor-arg>
    		<property name="requiresLowerCaseUrl" value="true" />
    	</bean>
    

    Configure the spring filter chain proxy to use our custom delegating URL Matcher, note that it’s important that “stripQueryStringFromUrls” is equal to false because Tapestry URLs contains crucial informations in the query string like the containing page for action request.

    	<bean id="springSecurityFilterChain" class="org.springframework.security.util.FilterChainProxy">
    		<property name="matcher" ref="delegatingUrlMatcher" />
    		<property name="stripQueryStringFromUrls" value="false" />
    		<property name="filterChainMap">
    			<map>
    				<entry key="/assets/.*">
    					<list>
    						<ref bean="httpSessionContextIntegrationFilter" />
    					</list>
    				</entry>
    				<entry key="/signin.*">
    					<list>
    						<ref bean="httpSessionContextIntegrationFilter" />
    					</list>
    				</entry>
    				<entry key="/signup.*">
    					<list>
    						<ref bean="httpSessionContextIntegrationFilter" />
    					</list>
    				</entry>
    				<entry key="/j_spring_security_logout.*">
    					<list>
    						<ref bean="httpSessionContextIntegrationFilter" />
    						<ref bean="logoutFilter" />
    					</list>
    				</entry>
    				<entry key="/j_spring_security_check.*">
    					<list>
    						<ref bean="httpSessionContextIntegrationFilter" />
    						<ref bean="authenticationProcessingFilter" />
    					</list>
    				</entry>
    
    				<!-- Public tapestry pages -->
    				<entry key="tapestry-public-page">
    					<list>
    						<ref bean="httpSessionContextIntegrationFilter" />
    					</list>
    				</entry>
    
    				<!-- All other resources must be a public Tapestry pages -->
    				<entry key="/.*">
    					<list>
    						<ref bean="httpSessionContextIntegrationFilter" />
    						<ref bean="securityContextHolderAwareRequestFilter" />
    						<ref bean="exceptionTranslationFilter" />
    						<ref bean="filterSecurityInterceptor" />
    					</list>
    				</entry>
    			</map>
    		</property>
    	</bean>
    

    To see all the code and configuration checkout the source on github.

    Conclusion

    We have demonstrated in this article that Tapestry can be integrated into other Frameworks using its Registry as the entry point. Of course our implementation only focus on Tapestry public pages and we assume that every actions on a public page is also public. But we can imagine more implementations of UrlMatcher for Tapestry, now it’s up to you.

    Thanks for reading.

No Comments »

RSS for comments - TrackBack URL

no comments yet.

Leave a comment

Spam Protection by WP-SpamFree