/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, 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.web.tomcat.security;

import java.io.IOException;
import java.security.Principal;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.security.auth.Subject;
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.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.jboss.logging.Logger;
import org.jboss.metadata.javaee.jboss.RunAsIdentityMetaData;
import org.jboss.metadata.web.jboss.JBossWebMetaData;
import org.jboss.security.AuthenticationManager;
import org.jboss.security.RunAsIdentity;
import org.jboss.security.plugins.JaasSecurityManagerServiceMBean;
import org.jboss.servlet.http.HttpEvent;

/**
 * A Valve that sets/clears the SecurityAssociation information associated with
 * the request thread for identity propagation.
 *
 * @author Scott.Stark@jboss.org
 * @author Thomas.Diesler@jboss.org
 * @author Anil.Saldhana@jboss.org
 * @version $Revision: 104333 $
 */
public class SecurityAssociationValve extends ValveBase
{
   private static Logger log = Logger.getLogger(SecurityAssociationValve.class);
   public static ThreadLocal<Principal> userPrincipal = new ThreadLocal<Principal>();
   /** Maintain the active WebMetaData for request security checks */
   public static ThreadLocal<JBossWebMetaData> activeWebMetaData = new ThreadLocal<JBossWebMetaData>();
   
   /** The web app metadata */
   private JBossWebMetaData metaData;
   /** The name in the session under which the Subject is stored */
   private String subjectAttributeName = null;
   /** The service used to flush authentication cache on session invalidation. */
   private JaasSecurityManagerServiceMBean secMgrService;
   private boolean trace;  

   public SecurityAssociationValve(JBossWebMetaData metaData,
      JaasSecurityManagerServiceMBean secMgrService)
   {
      this.metaData = metaData;
      this.secMgrService = secMgrService;
      this.trace = log.isTraceEnabled();
   }

   /**
    * The name of the request attribute under with the authenticated JAAS
    * Subject is stored on successful authentication. If null or empty then
    * the Subject will not be stored.
    */
   public void setSubjectAttributeName(String subjectAttributeName)
   {
      this.subjectAttributeName = subjectAttributeName;
      if (subjectAttributeName != null && subjectAttributeName.length() == 0)
         this.subjectAttributeName = null;
   }

   public void invoke(Request request, Response response)
           throws IOException, ServletException
   {
      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);

      if( trace )
         log.trace("Begin invoke, caller="+caller);
      
      try
      {
         Wrapper servlet = null;
         try
         {
            servlet = request.getWrapper();
            if (servlet != null)
            {
               String name = servlet.getName();
               RunAsIdentityMetaData identity = metaData.getRunAsIdentity(name);
               RunAsIdentity runAsIdentity = null;
               if(identity != null)
               {
                  if (trace)
                     log.trace(name + ", runAs: " + identity);
                  runAsIdentity = new RunAsIdentity(identity.getRoleName(),
                        identity.getPrincipalName(), identity.getRunAsRoles());
               }
               SecurityAssociationActions.pushRunAsIdentity(runAsIdentity); 
            }
            userPrincipal.set(caller);

            // 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) == false)
            {
               // Look to the session for the active caller security context
               if (session != null)
               {
                  principal =
                     (JBossGenericPrincipal) session.getPrincipal();
               }
            }
            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)
            {
               if (trace)
                  log.trace("Restoring principal info from cache");
               SecurityAssociationActions.setPrincipalInfo(principal.getAuthPrincipal(),
                  principal.getCredentials(), principal.getSubject());  
            }
            // Put the authenticated subject in the session if requested
            if (subjectAttributeName != null)
            {
               javax.naming.Context securityNamingCtx = getSecurityNamingContext();
               if (securityNamingCtx != null)
               {
                  // Get the JBoss security manager from the ENC context
                  AuthenticationManager securityMgr = (AuthenticationManager) securityNamingCtx.lookup("securityMgr");
                  Subject subject = securityMgr.getActiveSubject();
                  request.getRequest().setAttribute(subjectAttributeName, subject);
               }
            }
         }
         catch (Throwable e)
         {
            log.debug("Failed to determine servlet", e);
         }
         
         // Perform the request
         getNext().invoke(request, response);
         if(servlet != null)
         { 
            SecurityAssociationActions.popRunAsIdentity();
         }

         /* If the security domain cache is to be kept in synch with the
         session then flush the cache if the session has been invalidated.
         */
         if( secMgrService != null &&
            session != null && session.isValid() == false &&
            metaData.isFlushOnSessionInvalidation() == true )
         {
            if( principal != null )
            {
               String securityDomain = metaData.getSecurityDomain();
               if (trace)
               {
                  log.trace("Session is invalid, security domain: "+securityDomain
                     +", user="+principal);
               }
               try
               {
                  Principal authPrincipal = principal.getAuthPrincipal();
                  secMgrService.flushAuthenticationCache(securityDomain, authPrincipal);
               }
               catch(Exception e)
               {
                  log.debug("Failed to flush auth cache", e);
               }
            }
         } 
      }
      finally
      {
         if( trace )
            log.trace("End invoke, caller="+caller);
         userPrincipal.set(null); 
      }
   }

   private javax.naming.Context getSecurityNamingContext()
   {
      javax.naming.Context securityCtx = null;
      // Get the JBoss security manager from the ENC context
      try
      {
         InitialContext iniCtx = new InitialContext();
         securityCtx = (javax.naming.Context) iniCtx.lookup("java:comp/env/security");
      }
      catch (NamingException e)
      {
         // Apparently there is no security context?
      }
      return securityCtx;
   }

   public void event(Request request, Response response, HttpEvent event)
      throws IOException, ServletException
   {
      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);
      
      if( trace )
         log.trace("Begin invoke, caller="+caller);
      
      try
      {
         Wrapper servlet = null;
         try
         {
            servlet = request.getWrapper();
            if (servlet != null)
            {
               String name = servlet.getName();
               RunAsIdentityMetaData identity = metaData.getRunAsIdentity(name);
               RunAsIdentity runAsIdentity = null;
               if(identity != null)
               {
                  if (trace)
                     log.trace(name + ", runAs: " + identity);
                  runAsIdentity = new RunAsIdentity(identity.getRoleName(),
                        identity.getPrincipalName(), identity.getRunAsRoles());
               }
               SecurityAssociationActions.pushRunAsIdentity(runAsIdentity); 
            }
            userPrincipal.set(caller);
            
            // 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) == false)
            {
               // Look to the session for the active caller security context
               if (session != null)
               {
                  principal =
                     (JBossGenericPrincipal) session.getPrincipal();
               }
            }
            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)
            {
               if (trace)
                  log.trace("Restoring principal info from cache");
               SecurityAssociationActions.setPrincipalInfo(principal.getAuthPrincipal(),
                     principal.getCredentials(), principal.getSubject());  
            }
            // Put the authenticated subject in the session if requested
            if (subjectAttributeName != null)
            {
               javax.naming.Context securityNamingCtx = getSecurityNamingContext();
               if (securityNamingCtx != null)
               {
                  // Get the JBoss security manager from the ENC context
                  AuthenticationManager securityMgr = (AuthenticationManager) securityNamingCtx.lookup("securityMgr");
                  Subject subject = securityMgr.getActiveSubject();
                  request.getRequest().setAttribute(subjectAttributeName, subject);
               }
            }
         }
         catch (Throwable e)
         {
            log.debug("Failed to determine servlet", e);
         }
         
         // Perform the request
         getNext().event(request, response, event);
         if(servlet != null)
         { 
            SecurityAssociationActions.popRunAsIdentity();
         }
         
         /* If the security domain cache is to be kept in synch with the
    session then flush the cache if the session has been invalidated.
          */
         if( secMgrService != null &&
               session != null && session.isValid() == false &&
               metaData.isFlushOnSessionInvalidation() == true )
         {
            if( principal != null )
            {
               String securityDomain = metaData.getSecurityDomain();
               if (trace)
               {
                  log.trace("Session is invalid, security domain: "+securityDomain
                        +", user="+principal);
               }
               try
               {
                  Principal authPrincipal = principal.getAuthPrincipal();
                  secMgrService.flushAuthenticationCache(securityDomain, authPrincipal);
               }
               catch(Exception e)
               {
                  log.debug("Failed to flush auth cache", e);
               }
            }
         } 
      }
      finally
      {
         if( trace )
            log.trace("End invoke, caller="+caller);
         userPrincipal.set(null); 
      }
   }
      
}
