Liferay custom filter with after-filter

Goal

Create a Liferay custom filter that is executed after a specific previously existing Liferay filter

Description

This recipe explains the basic steps I took to implement a new Liferay filter for the path /documents/* that is executed after Liferay’s Auto Login Filter.

Although the reasoning of the DocumentFilter is not relevant for the recipe itself, I’m going to explain what it does here for better understanding. Basically, in the application, I have a custom service builder entity that is associated to a document that is uploaded/generated to the documents and media library. That document should be accessed only by specific groups, according to the organisation/group to which it belongs which may be several hundreds or even thousands.

A possible implementation would be the creation of system roles per organisation/group and make the documents be accessed to only those roles and associated the corresponding users to those roles. However, we didn’t like the idea of having to create so many system roles.

That is why we had the idea to create a documents filter that, based on the requested URL (the generated filename contains a very specific pattern which may be matched with a regular expression), checks if the associated entity may or not be granted access to the user. And that is also why we had the need to execute the DocumentFilter after the Auto Login Filter with com.liferay.portal.security.auth.BasicAuthHeaderAutoLogin included in the property auto.login.hooks of portal-ext.properties.

How to

Here goes the required steps:

  • DocumentFilter implementation:
    package com.linkare.myapp.web.filter;
    
    ... import statements
    
    /**
     *
     * @author Paulo Zenida - Linkare TI
     *
     */
    public class DocumentFilter extends BaseFilter {
    
        private static Log log = LogFactoryUtil.getLog(DocumentFilter.class);
    
        private static final String REQUEST_URI_SEPARATOR = "/";
    
        private static final int FILENAME_INDEX_ON_REQUEST_URI = 4;
    
        private String documentRegex;
    
        private int identificationNumberLength;
    
        @Override
        public void init(final FilterConfig filterConfig) {
    	super.init(filterConfig);
    
    	documentRegex = filterConfig.getInitParameter("documentRegex");
    	identificationNumberLength = Integer.valueOf(filterConfig.getInitParameter("identificationNumberLength"));
        }
    
        @Override
        protected void processFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
    	final String requestURI = request.getRequestURI();
    	final boolean isSpecificDocument = requestURI.matches(documentRegex);
    	if (isSpecificDocument) {
    	    final User currentUser = getUser(request);
    	    final String identificationNumber = getIdentificationNumber(requestURI);
    	    if (currentUser == null || !checkPermission(currentUser, isSpecificDocument, identificationNumber)) {
    		getLog().info(requestURI + " ACCESS DENIED FOR USER " + currentUser.getEmailAddress());
    		PortalUtil.sendError(HttpStatus.SC_UNAUTHORIZED, new PrincipalException("Access to this document is not granted"), request, response);
    		return;
    	    }
    	}
    	processFilter(DocumentFilter.class, request, response, filterChain);
        }
    
        private User getUser(final HttpServletRequest request) {
    	try {
    	    return PortalUtil.getUser(request);
    	} catch (PortalException | SystemException e) {
    	    return null;
    	}
        }
    
        private String getIdentificationNumber(final String requestURI) {
    	final String[] parts = requestURI.split(REQUEST_URI_SEPARATOR);
    	return parts[FILENAME_INDEX_ON_REQUEST_URI].substring(0, identificationNumberLength);
        }
    
        private boolean checkPermission(final User user, final boolean isSpecificDocument, final String identification) {
    	try {
    	    if (getPermissionChecker(user).isCompanyAdmin()) {
    		return true;
    	    }
    	    return checkAccess(user, identification);
    	} catch (final SystemException e) {
    	    e.printStackTrace();
    	}
    	return false;
        }
    
        private boolean checkAccess(final User user, final String identification) throws SystemException {
          ... business specific
        }
    
        private PermissionChecker getPermissionChecker(final User user) throws SystemException {
    	try {
    	    return PermissionCheckerFactoryUtil.create(user);
    	} catch (Exception e) {
    	    throw new SystemException(e);
    	}
        }
    
        @Override
        protected Log getLog() {
    	return log;
        }
    
  • DocumentFilter specification in liferay-hook.xml (look carefully the comment at the after-filter element!):
    <?xml version="1.0"?>
    <!DOCTYPE hook PUBLIC "-//Liferay//DTD Hook 6.2.0//EN" "http://www.liferay.com/dtd/liferay-hook_6_2_0.dtd">
    
    <hook>
    ...
    <servlet-filter>
    <servlet-filter-name>DocumentFilter</servlet-filter-name>
    <servlet-filter-impl>com.linkare.myapp.web.filter.DocumentFilter</servlet-filter-impl>
    <init-param>
    <param-name>documentRegex</param-name>
    <param-value>/documents/\d+/\d+/ABC\d{17}[.-].*</param-value>
    </init-param>
    <init-param>
    <param-name>identificationNumberLength</param-name>
    <param-value>20</param-value>
    </init-param>
    </servlet-filter>
    <servlet-filter-mapping>
    <strong>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;<servlet-filter-name>DocumentFilter</servlet-filter-name></strong>
    <strong>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;<after-filter>Header Filter</after-filter></strong>
    <!-- ATTENTION: It should be after the Auto Login Filter. But there is a bug in the InvokerFilterHelper class. Basically, there are 10 Auto Login Filter mappings and the one we are using, for /documents/* is one of the last. When we specify that our filter should be after the Auto Login Filter, that class will add it after the first occurrence of the Auto Login Filter mapping which is not the one we want. Therefore, in order to avoid this, we will register our filter after the first registered filter that comes after the Auto Login Filter -->
    <!--
    <after-filter>Auto Login Filter</after-filter>
    -->
    <url-pattern>/documents/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    </servlet-filter-mapping>
    </hook>
    
  • Relevant portal-ext.properties
    auto.login.hooks=... \
    com.liferay.portal.security.auth.BasicAuthHeaderAutoLogin
    com.liferay.portal.servlet.filters.autologin.AutoLoginFilter=true
    

Explanations

As explained in the comment before the auto-filter element of the XML snippet shown above, there is a bug in Liferay (at least, in version 6.2.GA6 CE) in the method registerFilterMapping of InvokerFilterHelper class (shown next) that will register the DocumentFilter after the first occurrence of the Auto Login Filter mapping (there are currently 10 filter mappings for the Auto Login Filter in liferay-web.xml) which is not the most appropriate, as it should be registered after the last occurrence of it. So, in order to fix this, and make sure DocumentFilter is executed after the Auto Login Filter, I changed its definition to be executed after the filter that is registered after the Auto Login Filter, i.e., the Header Filter.

...
public void registerFilterMapping(
  FilterMapping filterMapping, String filterName, boolean after) {

  int i = 0;
  if (Validator.isNotNull(filterName)) {
    Filter filter = _filters.get(filterName);

    if (filter != null) {
      for (; i &amp;lt; _filterMappings.size(); i++) {
        FilterMapping currentFilterMapping = _filterMappings.get(i);
        /*
         * BUG HERE:
         * It should not break at this point.
         * After finding the specified filter (Auto Login Filter), it should
         * go till the next that it gets changed (Header Filter). Or maybe
         * do the search backwards so that we always find the last occurrence.
        */
        if (currentFilterMapping.getFilter() == filter) {
          break;
        }
      }
    }
  }

  if (after) {
    i++;
  }

  _filterMappings.add(i, filterMapping);

  for (InvokerFilter invokerFilter : _invokerFilters) {
    invokerFilter.clearFilterChainsCache();
  }
}
...

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s