/*
* JBoss, the OpenSource J2EE webOS
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.web.tomcat.security;

import java.io.IOException;
import java.security.Principal;
import java.security.Permission;
import java.security.ProtectionDomain;
import java.security.Policy;
import java.security.CodeSource;
import java.util.Set;
import java.util.List;

import javax.security.jacc.WebUserDataPermission;
import javax.security.jacc.PolicyContext;
import javax.security.jacc.WebResourcePermission;
import javax.security.jacc.WebRoleRefPermission;
import javax.security.jacc.PolicyContextException;
import javax.security.auth.Subject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.deploy.SecurityConstraint;
import org.jboss.logging.Logger;
import org.jboss.metadata.WebMetaData;
import org.jboss.metadata.SecurityRoleRefMetaData;

/** A subclass of JBossSecurityMgrRealm that peforms authorization based on
 * the JACC permissions and active Policy implementation.
 * 
 * @author Scott.Stark@jboss.org
 * @version $Revision: 1.1.4.7 $
 */
public class JaccAuthorizationRealm extends JBossSecurityMgrRealm
{
   static Logger log = Logger.getLogger(JaccAuthorizationRealm.class);

   /** The JACC PolicyContext key for the current Subject */
   private static final String SUBJECT_CONTEXT_KEY = "javax.security.auth.Subject.container";
   /** The current servlet request */
   private static ThreadLocal activeRequest = new ThreadLocal();
   private boolean trace;
   private Policy policy;

   public JaccAuthorizationRealm()
   {
      policy = Policy.getPolicy();
      trace = log.isTraceEnabled();
   }

   public boolean hasResourcePermission(Request request, Response response,
      SecurityConstraint[] securityConstraints, Context context)
      throws IOException
   {
      Wrapper servlet = request.getWrapper();
      if (servlet != null)
      {
         activeRequest.set(servlet.getName());
      }
      HttpServletRequest httpRequest = (HttpServletRequest) request.getRequest();
      WebResourcePermission perm = new WebResourcePermission(httpRequest);
      boolean allowed = checkSecurityAssociation(perm, request.getUserPrincipal());
      if( trace )
         log.trace("hasResourcePermission, perm="+perm+", allowed="+allowed);
      if( allowed == false )
      {
         response.sendError(HttpServletResponse.SC_FORBIDDEN,
            sm.getString("realmBase.forbidden"));
      }
      return allowed;
   }

   public boolean hasRole(Principal principal, String name)
   {
      // 
      String servletName = (String) activeRequest.get();
      WebMetaData metaData = (WebMetaData) SecurityAssociationValve.activeWebMetaData.get();
      List roleRefs = metaData.getSecurityRoleRefs(servletName);
      String roleName = name;
      for(int n = 0; n < roleRefs.size(); n ++)
      {
         SecurityRoleRefMetaData ref = (SecurityRoleRefMetaData) roleRefs.get(n);
         if( ref.getLink().equals(name) )
         {
            roleName = ref.getName();
            break;
         }
      }
      WebRoleRefPermission perm = new WebRoleRefPermission(servletName, roleName);
      Principal[] principals = {principal};
      Set roles = getPrincipalRoles(principal);
      if( roles != null )
      {
         principals = new Principal[roles.size()];
         roles.toArray(principals);
      }
      boolean allowed = checkSecurityAssociation(perm, principals);
      if( trace )
         log.trace("hasRole, perm="+perm+", allowed="+allowed);
      return allowed;
   }

   public boolean hasUserDataPermission(Request request, Response response,
      SecurityConstraint[] constraints) throws IOException
   {
      HttpServletRequest httpRequest = (HttpServletRequest) request.getRequest();
      Principal requestPrincpal = httpRequest.getUserPrincipal();
      establishSubjectContext(requestPrincpal);
      WebUserDataPermission perm = new WebUserDataPermission(httpRequest);
      if( trace )
         log.trace("hasUserDataPermission, p="+perm);
      boolean ok = false;
      try
      {
         Principal[] principals = null;
         ok = checkSecurityAssociation(perm, principals);
      }
      catch(Exception e)
      {
         if( trace )
            log.trace("Failed to checkSecurityAssociation", e);
      }

      /* If the constraint is not valid delegate to super to redirect to the
      ssl port if allowed
      */
      if( ok == false )
         ok = super.hasUserDataPermission(request, response, constraints);
      return ok;
   }

   /** See if the given JACC permission is implied using the caller as
    * obtained from either the
    * PolicyContext.getContext(javax.security.auth.Subject.container) or
    * the info associated with the requestPrincipal.
    * 
    * @param perm - the JACC permission to check
    * @param requestPrincpal - the http request getUserPrincipal
    * @return true if the permission is allowed, false otherwise
    */ 
   private boolean checkSecurityAssociation(Permission perm, Principal requestPrincpal)
   {
      // Get the caller
      Subject caller = establishSubjectContext(requestPrincpal);

      // Get the caller principals, its null if there is no caller
      Principal[] principals = null;
      if( caller != null )
      {
         if( trace )
            log.trace("No active subject found, using ");
         Set principalsSet = caller.getPrincipals();
         principals = new Principal[principalsSet.size()];
         principalsSet.toArray(principals);
      }
      return checkSecurityAssociation(perm, principals);
   }
   /** See if the given permission is implied by the Policy. This calls
    * Policy.implies(pd, perm) with the ProtectionDomain built from the
    * active CodeSource set by the JaccContextValve, and the given
    * principals.
    * 
    * @param perm - the JACC permission to evaluate
    * @param principals - the possibly null set of principals for the caller
    * @return true if the permission is allowed, false otherwise
    */ 
   private boolean checkSecurityAssociation(Permission perm, Principal[] principals)
   {
      CodeSource webCS = (CodeSource) JaccContextValve.activeCS.get();
      ProtectionDomain pd = new ProtectionDomain(webCS, null, null, principals);
      boolean allowed = policy.implies(pd, perm);
      if( trace )
      {
         String msg = (allowed ? "Allowed: " : "Denied: ") +perm;
         log.trace(msg);
      }
      return allowed;
   }

   /**
    * Ensure that the JACC PolicyContext Subject handler has access to the
    * authenticated Subject. The caching of the authentication state by tomcat
    * means that we need to retrieve the Subject from the JBossGenericPrincipal
    * if the realm was not invoked to authenticate the caller.
    * 
    * @param principal - the http request getUserPrincipal
    * @return the authenticated Subject is there is one, null otherwise
    */ 
   private Subject establishSubjectContext(Principal principal)
   {
      Subject caller = null;
      try
      {
         caller = (Subject) PolicyContext.getContext(SUBJECT_CONTEXT_KEY);
      }
      catch (PolicyContextException e)
      {
         if( trace )
            log.trace("Failed to get subject from PolicyContext", e);
      }

      if( caller == null )
      {
         // Test the request principal that may come from the session cache 
         if( principal instanceof JBossGenericPrincipal )
         {
            JBossGenericPrincipal jgp = (JBossGenericPrincipal) principal;
            caller = jgp.getSubject();
            // 
            if (trace)
               log.trace("Restoring principal info from cache");
            SecurityAssociationActions.setPrincipalInfo(jgp.getAuthPrincipal(),
               jgp.getCredentials(), jgp.getSubject());
         }
      }
      return caller;
   }
}
