mitm.djigzo.web.services.SecurityModule.java Source code

Java tutorial

Introduction

Here is the source code for mitm.djigzo.web.services.SecurityModule.java

Source

/*
 * Copyright (c) 2008-2011, Martijn Brinkers, Djigzo.
 * 
 * This file is part of Djigzo email encryption.
 *
 * Djigzo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License 
 * version 3, 19 November 2007 as published by the Free Software 
 * Foundation.
 *
 * Djigzo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public 
 * License along with Djigzo. If not, see <http://www.gnu.org/licenses/>
 *
 * Additional permission under GNU AGPL version 3 section 7
 * 
 * If you modify this Program, or any covered work, by linking or 
 * combining it with saaj-api-1.3.jar, saaj-impl-1.3.jar, 
 * wsdl4j-1.6.1.jar (or modified versions of these libraries), 
 * containing parts covered by the terms of Common Development and 
 * Distribution License (CDDL), Common Public License (CPL) the 
 * licensors of this Program grant you additional permission to 
 * convey the resulting work.
 */
package mitm.djigzo.web.services;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import mitm.application.djigzo.ws.EventLoggerWS;
import mitm.common.event.EventLogger;
import mitm.common.event.EventLoggerFactory;
import mitm.common.security.crypto.RandomGenerator;
import mitm.common.security.crypto.impl.RandomGeneratorImpl;
import mitm.djigzo.web.common.RedirectRequestExceptionHandler;
import mitm.djigzo.web.common.event.RemoteEventLoggerImpl;
import mitm.djigzo.web.services.security.AuthenticationEventListener;
import mitm.djigzo.web.services.security.CSRFFilter;
import mitm.djigzo.web.services.security.CSRFFilterImpl;
import mitm.djigzo.web.services.security.HMACFilter;
import mitm.djigzo.web.services.security.HMACFilterImpl;
import mitm.djigzo.web.services.security.LogoutServiceImpl;
import mitm.djigzo.web.services.security.RoleFilter;
import mitm.djigzo.web.services.security.RoleFilterImpl;
import mitm.djigzo.web.services.security.SecurityChecker;
import mitm.djigzo.web.services.security.SpringSecurityWorker;
import mitm.djigzo.web.services.security.StaticSecurityChecker;
import mitm.djigzo.web.utils.PasswordCodec;
import mitm.djigzo.web.utils.PasswordCodecImpl;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.internal.services.LinkFactory;
import org.apache.tapestry5.internal.services.RequestPageCache;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.OrderedConfiguration;
import org.apache.tapestry5.ioc.ServiceBinder;
import org.apache.tapestry5.ioc.annotations.EagerLoad;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Match;
import org.apache.tapestry5.ioc.annotations.Value;
import org.apache.tapestry5.services.ApplicationStateManager;
import org.apache.tapestry5.services.ComponentClassTransformWorker;
import org.apache.tapestry5.services.ComponentEventRequestFilter;
import org.apache.tapestry5.services.HttpServletRequestFilter;
import org.apache.tapestry5.services.HttpServletRequestHandler;
import org.apache.tapestry5.services.MarkupRendererFilter;
import org.apache.tapestry5.services.PartialMarkupRendererFilter;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.RequestExceptionHandler;
import org.apache.tapestry5.services.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.AccessDecisionManager;
import org.springframework.security.AccessDeniedException;
import org.springframework.security.AuthenticationManager;
import org.springframework.security.providers.encoding.PasswordEncoder;
import org.springframework.security.ui.logout.LogoutHandler;
import org.springframework.security.ui.logout.SecurityContextLogoutHandler;

/**
 * Tapestry module for most security related services
 * 
 * @author Martijn Brinkers
 *
 */
public class SecurityModule {
    private final static Logger logger = LoggerFactory.getLogger(SecurityModule.class);

    public static void bind(final ServiceBinder binder) {
        binder.bind(LogoutService.class, LogoutServiceImpl.class);
        binder.bind(RandomGenerator.class, RandomGeneratorImpl.class);
        binder.bind(RoleFilter.class, RoleFilterImpl.class);
    }

    public static void contributeLogoutService(final OrderedConfiguration<LogoutHandler> cfg) {
        cfg.add("securityContextLogoutHandler", new SecurityContextLogoutHandler());
    }

    public static void contributeComponentClassTransformWorker(
            OrderedConfiguration<ComponentClassTransformWorker> configuration, SecurityChecker securityChecker) {
        /*
         * Make the SpringSecurityWorker the last one so the security check is done first.
         * 
         * We need to add it just before UnclaimedField otherwise well get a warning like:
         * 
         * "WARN  Unable to add 'UnclaimedField' as a dependency of 'SpringSecurityWorker', 
         * as that forms a dependency cycle ('SpringSecurityWorker' depends on itself via 
         * 'UnclaimedField'). The dependency has been ignored."
         * 
         * It seems that UnclaimedField should always be the last one
         */
        configuration.add("SpringSecurityWorker", new SpringSecurityWorker(securityChecker),
                "before:UnclaimedField");
    }

    public static SecurityChecker buildSecurityChecker(final AccessDecisionManager accessDecisionManager,
            final AuthenticationManager authenticationManager) throws Exception {
        StaticSecurityChecker checker = new StaticSecurityChecker();

        checker.setAccessDecisionManager(accessDecisionManager);
        checker.setAuthenticationManager(authenticationManager);
        checker.afterPropertiesSet();

        return checker;
    }

    public static void contributeFactoryDefaults(MappedConfiguration<String, String> configuration) {
        configuration.add("login-processing-url", "/check");
    }

    /*
     * RequestExceptionHandler that intercepts AccessDeniedException and redirects
     * to the provided page.
     */
    private static class AccessDeniedRedirectRequestExceptionHandler
            extends RedirectRequestExceptionHandler<AccessDeniedException> {
        public AccessDeniedRedirectRequestExceptionHandler(RequestExceptionHandler delegate, Response response,
                LinkFactory linkFactory, String redirectToPage, Object... context) {
            super(delegate, response, linkFactory, redirectToPage, context);
        }
    }

    /*
     * RequestExceptionHandler that intercepts and access denied related exceptions
     */
    @Match(value = { "RequestExceptionHandler" })
    public static RequestExceptionHandler decorateAccessDeniedRequestExceptionHandler(final Object delegate,
            final Response response, final LinkFactory linkFactory,
            @Inject @Value("${access-denied-page}") final String redirectToPage) {
        return new AccessDeniedRedirectRequestExceptionHandler((RequestExceptionHandler) delegate, response,
                linkFactory, redirectToPage);
    }

    /*
     * Builds the 'Cross Site Request Forgeries' (CSRF) filter.
     * 
     * Note: must be EagerLoad'ed because we want the LinkFactory listener to be active from the
     * start.
     */
    @EagerLoad
    public static CSRFFilter buildCSRFLinkFactoryListener(ApplicationStateManager asm, Request request,
            Response response, LinkFactory linkFactory, RequestPageCache requestPageCache,
            @Inject @Value("${csrf.redirectTo}") String redirectTo) {
        CSRFFilterImpl filter = new CSRFFilterImpl(asm, request, response, linkFactory, requestPageCache,
                redirectTo);

        linkFactory.addListener(filter);

        return filter;
    }

    /*
     * This factory is used to inject a Tapestry bean into a spring bean. That's why it has to be 
     * eager loaded.
     */
    @EagerLoad
    public EventLoggerFactory buildEventLoggerFactory(AuthenticationEventListener authenticationEventListener,
            final EventLoggerWS eventLoggerWS) {
        EventLoggerFactory factory = new EventLoggerFactory() {
            @Override
            public EventLogger create() {
                return new RemoteEventLoggerImpl(eventLoggerWS);
            }
        };

        authenticationEventListener.setEventLoggerFactory(factory);

        return factory;
    }

    /*
     * Builds the PasswordCodec service
     */
    public PasswordCodec buildPasswordCodec(PasswordEncoder passwordEncoder) {
        return new PasswordCodecImpl(passwordEncoder);
    }

    public void contributeComponentEventRequestHandler(
            OrderedConfiguration<ComponentEventRequestFilter> configuration, CSRFFilter csrfFilter,
            HMACFilter hMACFilter, RoleFilter roleFilter) {
        /*
         * Make sure the CSRF filter is injected before anything else
         */
        configuration.add("CSRFFilter", csrfFilter, "after:UploadException", "before:Ajax");
        configuration.add("HMACFilter", hMACFilter, "after:CSRFFilter", "before:Ajax");
        configuration.add("RoleFilter", roleFilter, "after:HMACFilter", "before:Ajax");
    }

    public static HMACFilter buildHMACFilter(ApplicationStateManager asm, Request request, Response response,
            LinkFactory linkFactory, RequestPageCache requestPageCache,
            @Inject @Value("${hmac.redirectTo}") String redirectTo) {
        String[] protectedElements = { Form.FORM_DATA, "t:state:client" };

        HMACFilter filter = new HMACFilterImpl(asm, request, response, linkFactory, requestPageCache, redirectTo,
                protectedElements);

        return filter;
    }

    public void contributeMarkupRenderer(OrderedConfiguration<MarkupRendererFilter> configuration,
            HMACFilter hMACFilter) {
        configuration.add("HMACFilter", hMACFilter, "after:*");
    }

    public void contributePartialMarkupRenderer(OrderedConfiguration<PartialMarkupRendererFilter> configuration,
            HMACFilter hMACFilter) {
        configuration.add("HMACFilter", hMACFilter, "after:*");
    }

    private static final String[] ASSET_WHITE_LIST = { "jpg", "jpeg", "png", "gif", "js", "css", "ico" };

    /*
     * All the assets that are allowed to be downloaded using the assets service (including files without extension and dirs)
     */
    private static final Set<String> assetsWhitelist = Collections
            .synchronizedSet(new HashSet<String>(Arrays.asList(ASSET_WHITE_LIST)));

    public void contributeHttpServletRequestHandler(OrderedConfiguration<HttpServletRequestFilter> configuration,
            @Inject @Value("${access-denied-page}") final String accessDeniedPage) {
        /*
         * Create a filter that will block access to some assets. The asset service allows access to some assets we do
         * not want to expose. The asset service will show all files in /assets/ directory and allows you (by default)
         * to download some files which you do not want to expose.
         */
        HttpServletRequestFilter filter = new HttpServletRequestFilter() {
            @Override
            public boolean service(HttpServletRequest request, HttpServletResponse response,
                    HttpServletRequestHandler handler) throws IOException {
                String path = request.getServletPath();

                if (path.startsWith("/assets")
                        && (!assetsWhitelist.contains(StringUtils.lowerCase(FilenameUtils.getExtension(path))))) {
                    logger.warn("access to asset " + path + " denied");

                    response.sendRedirect(request.getContextPath() + "/" + accessDeniedPage);

                    return true;
                }

                return handler.service(request, response);
            }
        };

        configuration.add("AssetProtectionFilter", filter, "before:*");
    }
}