/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.web.security;

import java.io.IOException;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.util.Map;

import javax.security.jacc.PolicyContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpSession;

import org.apache.catalina.Manager;
import org.apache.catalina.Session;
import org.apache.catalina.Wrapper;
import org.apache.catalina.authenticator.Constants;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.as.web.WebLogger;
import org.jboss.as.web.deployment.WarMetaData;
import org.jboss.metadata.javaee.jboss.RunAsIdentityMetaData;
import org.jboss.metadata.web.jboss.JBossWebMetaData;
import org.jboss.security.RunAsIdentity;
import org.jboss.security.SecurityConstants;
import org.jboss.security.SecurityContext;
import org.jboss.security.SecurityRolesAssociation;
import org.jboss.security.SecurityUtil;
import org.jboss.security.SimplePrincipal;

/**
 * A {@code Valve} that creates a {@code SecurityContext} if one doesn't exist and sets the security information based on the
 * authenticated principal in the request's session.
 *
 * @author <a href="mailto:mmoyses@redhat.com">Marcus Moyses</a>
 * @author Scott.Stark@jboss.org
 * @author Thomas.Diesler@jboss.org
 * @author Anil.Saldhana@jboss.org
 */
public class SecurityContextAssociationValve extends ValveBase {

    private final String securityDomain;
    private final Map<String, RunAsIdentityMetaData> runAsIdentity;
    private final String contextId;

    private static final ThreadLocal<Request> activeRequest = new ThreadLocal<Request>();

    public SecurityContextAssociationValve(DeploymentUnit deploymentUnit) {
        final WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
        JBossWebMetaData metaData = warMetaData.getMergedJBossWebMetaData();
        String securityDomain = SecurityUtil.unprefixSecurityDomain(metaData.getSecurityDomain());
        if (securityDomain == null) {
            securityDomain = SecurityConstants.DEFAULT_WEB_APPLICATION_POLICY;
        }
        String contextId = deploymentUnit.getName();
        if (deploymentUnit.getParent() != null) {
            contextId = deploymentUnit.getParent().getName() + "!" + contextId;
        }
        this.securityDomain = securityDomain;
        this.runAsIdentity = metaData.getRunAsIdentity();
        this.contextId = contextId;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        activeRequest.set(request);

        Session session = null;
        // Get the request caller which could be set due to SSO
        Principal caller = request.getPrincipal();
        // The cached web container principal
        JBossGenericPrincipal principal = null;
        HttpSession hsession = request.getSession(false);

        WebLogger.WEB_SECURITY_LOGGER.tracef("Begin invoke, caller=%s", caller);

        boolean createdSecurityContext = false;
        SecurityContext sc = SecurityActions.getSecurityContext();
        if (sc == null) {
            createdSecurityContext = true;
            sc = SecurityActions.createSecurityContext(securityDomain);
            SecurityActions.setSecurityContextOnAssociation(sc);
        }

        String previousContextID = null;
        try {
            Wrapper servlet = null;
            try {
                servlet = request.getWrapper();
                if (servlet != null) {
                    String name = servlet.getName();
                    RunAsIdentityMetaData identity = runAsIdentity.get(name);
                    RunAsIdentity runAsIdentity = null;
                    if (identity != null) {
                        WebLogger.WEB_SECURITY_LOGGER.tracef("%s, runAs: %s", name, identity);
                        runAsIdentity = new RunAsIdentity(identity.getRoleName(), identity.getPrincipalName(),
                                identity.getRunAsRoles());
                    }
                    SecurityActions.pushRunAsIdentity(runAsIdentity, sc);
                }

                // If there is a session, get the tomcat session for the principal
                Manager manager = container.getManager();
                if (manager != null && hsession != null) {
                    try {
                        session = manager.findSession(hsession.getId());
                    } catch (IOException ignore) {
                    }
                }

                if (caller == null || !(caller instanceof JBossGenericPrincipal)) {
                    // Look to the session for the active caller security context
                    if (session != null) {
                        principal = (JBossGenericPrincipal) session.getPrincipal();
                    }
                    if (principal == null) {
                        Session sessionInternal = request.getSessionInternal(false);
                        if (sessionInternal != null) {
                            principal = (JBossGenericPrincipal) sessionInternal.getNote(Constants.FORM_PRINCIPAL_NOTE);
                        }
                    }
                } else {
                    // Use the request principal as the caller identity
                    principal = (JBossGenericPrincipal) caller;
                }

                // If there is a caller use this as the identity to propagate
                if (principal != null) {
                    WebLogger.WEB_SECURITY_LOGGER.tracef("Restoring principal info from cache");
                    if (createdSecurityContext) {
                        sc.getUtil().createSubjectInfo(new SimplePrincipal(principal.getName()), principal.getCredentials(),
                                principal.getSubject());
                    }
                }
            } catch (Throwable e) {
                //TODO:decide whether to log this as info or warn
                WebLogger.WEB_SECURITY_LOGGER.debug("Failed to determine servlet", e);
            }
            // set JACC contextID
            previousContextID = setContextID(contextId);

            // Perform the request
            getNext().invoke(request, response);
            if (servlet != null) {
                SecurityActions.popRunAsIdentity(sc);
            }
        } finally {
            WebLogger.WEB_SECURITY_LOGGER.tracef("End invoke, caller=%s", caller);
            SecurityActions.clearSecurityContext();
            SecurityRolesAssociation.setSecurityRoles(null);
            setContextID(previousContextID);
            activeRequest.set(null);
        }
    }

    public static Request getActiveRequest() {
        return activeRequest.get();
    }

    private static class SetContextIDAction implements PrivilegedAction<String> {

        private String contextID;

        SetContextIDAction(String contextID) {
            this.contextID = contextID;
        }

        @Override
        public String run() {
            String currentContextID = PolicyContext.getContextID();
            PolicyContext.setContextID(this.contextID);
            return currentContextID;
        }
    }

    private String setContextID(String contextID) {
        if (System.getSecurityManager() == null) {
            String currentContextID = PolicyContext.getContextID();
            PolicyContext.setContextID(contextID);
            return currentContextID;
        } else {
            PrivilegedAction<String> action = new SetContextIDAction(contextID);
            return AccessController.doPrivileged(action);
        }
    }
}
