/*
 * Copyright 2010 the original author or authors.
 *
 * 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.jaasapi;

import java.io.IOException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;

import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.springframework.security.authentication.jaas.JaasAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

/**
 * <p>
 * A <code>Filter</code> which attempts to obtain a JAAS <code>Subject</code>
 * and continue the <code>FilterChain</code> running as that
 * <code>Subject</code>.
 * </p>
 * <p>
 * By using this <code>Filter</code> in conjunction with Spring's
 * <code>JaasAuthenticationProvider</code> both Spring's
 * <code>SecurityContext</code> and a JAAS <code>Subject</code> can be populated
 * simultaneously. This is useful when integrating with code that requires a
 * JAAS <code>Subject</code> to be populated.
 * </p>
 *
 * @author Rob Winch
 * @see #doFilter(ServletRequest, ServletResponse, FilterChain)
 * @see #obtainSubject(ServletRequest)
 */
public class JaasApiIntegrationFilter extends GenericFilterBean {
    //~ Instance fields ================================================================================================

    private boolean createEmptySubject;

    //~ Methods ========================================================================================================

    /**
     * <p>
     * Attempts to obtain and run as a JAAS <code>Subject</code> using
     * {@link #obtainSubject(ServletRequest)}.
     * </p>
     *
     * <p>
     * If the <code>Subject</code> is <code>null</code> and
     * <tt>createEmptySubject</tt> is <code>true</code>, an empty, writeable
     * <code>Subject</code> is used. This allows for the <code>Subject</code> to
     * be populated at the time of login. If the <code>Subject</code> is
     * <code>null</code>, the <code>FilterChain</code> continues with no
     * additional processing. If the <code>Subject</code> is not
     * <code>null</code>, the <code>FilterChain</code> is ran with
     * {@link Subject#doAs(Subject, PrivilegedExceptionAction)} in conjunction
     * with the <code>Subject</code> obtained.
     * </p>
     */
    public final void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
            throws ServletException, IOException {

        Subject subject = obtainSubject(request);
        if (subject == null && createEmptySubject) {
            if (logger.isDebugEnabled()) {
                logger.debug("Subject returned was null and createEmtpySubject is true; creating new empty subject to run as.");
            }
            subject = new Subject();
        }
        if (subject == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("Subject is null continue running with no Subject.");
            }
            chain.doFilter(request, response);
            return;
        }
        final PrivilegedExceptionAction<Object> continueChain = new PrivilegedExceptionAction<Object>() {
            public Object run() throws IOException, ServletException {
                chain.doFilter(request, response);
                return null;
            }
        };

        if (logger.isDebugEnabled()) {
            logger.debug("Running as Subject " + subject);
        }
        try {
            Subject.doAs(subject, continueChain);
        } catch (PrivilegedActionException e) {
            throw new ServletException(e.getMessage(), e);
        }
    }

    /**
     * <p>
     * Obtains the <code>Subject</code> to run as or <code>null</code> if no
     * <code>Subject</code> is available.
     * </p>
     * <p>
     * The default implementation attempts to obtain the <code>Subject</code>
     * from the <code>SecurityContext</code>'s <code>Authentication</code>. If
     * it is of type <code>JaasAuthenticationToken</code> and is authenticated,
     * the <code>Subject</code> is returned from it. Otherwise,
     * <code>null</code> is returned.
     * </p>
     *
     * @param request
     *            the current <code>ServletRequest</code>
     * @return the Subject to run as or <code>null</code> if no
     *         <code>Subject</code> is available.
     */
    protected Subject obtainSubject(ServletRequest request) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (logger.isDebugEnabled()) {
            logger.debug("Attempting to obtainSubject using authentication : " + authentication);
        }
        if (authentication == null) {
            return null;
        }
        if (!authentication.isAuthenticated()) {
            return null;
        }
        if (!(authentication instanceof JaasAuthenticationToken)) {
            return null;
        }
        JaasAuthenticationToken token = (JaasAuthenticationToken) authentication;
        LoginContext loginContext = token.getLoginContext();
        if (loginContext == null) {
            return null;
        }
        return loginContext.getSubject();
    }

    /**
     * Sets <code>createEmptySubject</code>. If the value is <code>true</code>,
     * and {@link #obtainSubject(ServletRequest)} returns <code>null</code>, an
     * empty, writeable <code>Subject</code> is created instead. Otherwise no
     * <code>Subject</code> is used. The default is <code>false</code>.
     *
     * @param createEmptySubject
     *            the new value
     */
    public final void setCreateEmptySubject(boolean createEmptySubject) {
        this.createEmptySubject = createEmptySubject;
    }
}