/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt 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.security.Principal;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.security.auth.Subject;

import EDU.oswego.cs.dl.util.concurrent.ConcurrentReaderHashMap;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Realm;
import org.apache.catalina.realm.RealmBase;
import org.jboss.logging.Logger;
import org.jboss.security.CertificatePrincipal;
import org.jboss.security.RealmMapping;
import org.jboss.security.SimplePrincipal;
import org.jboss.security.SubjectSecurityManager;
import org.jboss.security.auth.certs.SubjectDNMapping;

/**
 * An implementation of the catelinz Realm and Valve interfaces. The Realm
 * implementation handles authentication and authorization using the JBossSX
 * security framework. It relieas on the JNDI ENC namespace setup by the
 * AbstractWebContainer. In particular, it uses the java:comp/env/security
 * subcontext to access the security manager interfaces for authorization and
 * authenticaton.
 * <p/>
 * The Valve interface is used to associated the authenticated user with the
 * SecurityAssociation class when a request begins so that web components may
 * call EJBs and have the principal propagated. The security association is
 * removed when the request completes.
 * 
 * @author Scott.Stark@jboss.org
 * @version $Revision: 1.1.1.1.2.15 $
 * @see org.jboss.security.AuthenticationManager
 * @see org.jboss.security.CertificatePrincipal
 * @see org.jboss.security.RealmMapping
 * @see org.jboss.security.SimplePrincipal
 * @see org.jboss.security.SecurityAssociation
 * @see org.jboss.security.SubjectSecurityManager
 */
public class JBossSecurityMgrRealm extends RealmBase implements Realm
{
   static Logger log = Logger.getLogger(JBossSecurityMgrRealm.class);
   /**
    * The converter from X509 cert chain to Princpal
    */
   private CertificatePrincipal certMapping = new SubjectDNMapping();
   /**
    * ConcurrentReaderHashMap<UserPrincipal, AuthPrincipal>
    */
   private ConcurrentReaderHashMap userPrincipalMap = new ConcurrentReaderHashMap();
   /**
    * The JBossSecurityMgrRealm category trace flag
    */
   private boolean trace;

   /**
    * Set the class name of the CertificatePrincipal used for mapping
    * X509 cert chains to a Princpal.
    * 
    * @param className the CertificatePrincipal implementation class that must
    *                  have a no-arg ctor.
    * @see org.jboss.security.CertificatePrincipal
    */
   public void setCertificatePrincipal(String className)
   {
      try
      {
         ClassLoader loader = Thread.currentThread().getContextClassLoader();
         Class cpClass = loader.loadClass(className);
         certMapping = (CertificatePrincipal) cpClass.newInstance();
      }
      catch (Exception e)
      {
         log.error("Failed to load CertificatePrincipal: " + className, e);
         certMapping = new SubjectDNMapping();
      }
   }

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

   /**
    * Override to allow a single realm to be shared as a realm and valve
    */
   public void start() throws LifecycleException
   {
      if (super.started == true)
         return;
      super.start();
      trace = log.isTraceEnabled();
   }

   /**
    * Override to allow a single realm to be shared as a realm and valve
    */
   public void stop() throws LifecycleException
   {
      if (super.started == false)
         return;
      super.stop();
   }

   /**
    * Return the Principal associated with the specified chain of X509
    * client certificates.  If there is none, return <code>null</code>.
    * 
    * @param certs Array of client certificates, with the first one in
    *              the array being the certificate of the client itself.
    */
   public Principal authenticate(X509Certificate[] certs)
   {
      Principal principal = null;
      Context securityCtx = getSecurityContext();
      if (securityCtx == null)
      {
         if (trace)
            log.trace("No security context for authenticate(X509Certificate[])");
         return null;
      }

      try
      {
         // Get the JBoss security manager from the ENC context
         SubjectSecurityManager securityMgr = (SubjectSecurityManager) securityCtx.lookup("securityMgr");
         Subject subject = new Subject();
         principal = certMapping.toPrinicipal(certs);
         if (securityMgr.isValid(principal, certs))
         {
            if (trace)
               log.trace("User: " + principal + " is authenticated");
            SecurityAssociationActions.setPrincipalInfo(principal, certs, subject);
            // Get the CallerPrincipal mapping
            RealmMapping realmMapping = (RealmMapping) securityCtx.lookup("realmMapping");
            Principal oldPrincipal = principal;
            principal = realmMapping.getPrincipal(oldPrincipal);
            if (trace)
            {
               log.trace("Mapped from input principal: " + oldPrincipal
                  + "to: " + principal);
            }
            if (principal.equals(oldPrincipal) == false)
            {
               userPrincipalMap.put(principal, oldPrincipal);
            }
            // Get the caching principal
            principal = getCachingPrincpal(realmMapping, oldPrincipal, principal,
               certs, subject);
         }
         else
         {
            if (trace)
               log.trace("User: " + principal + " is NOT authenticated");
            principal = null;
         }
      }
      catch (NamingException e)
      {
         log.error("Error during authenticate", e);
      }
      return principal;
   }

   /**
    * Return the Principal associated with the specified username, which
    * matches the digest calculated using the given parameters using the
    * method described in RFC 2069; otherwise return <code>null</code>.
    * 
    * @param username Username of the Principal to look up
    * @param digest   Digest which has been submitted by the client
    * @param nonce    Unique (or supposedly unique) token which has been used
    *                 for this request
    * @param realm    Realm name
    * @param md5a2    Second MD5 digest used to calculate the digest :
    *                 MD5(Method + ":" + uri)
    */
   public Principal authenticate(String username, String digest, String nonce,
      String nc, String cnonce, String qop, String realm, String md5a2)
   {
      return super.authenticate(username, digest, nonce,
         nc, cnonce, qop, realm, md5a2);
   }

   /**
    * Return the Principal associated with the specified username and
    * credentials, if there is one; otherwise return <code>null</code>.
    * 
    * @param username    Username of the Principal to look up
    * @param credentials Password or other credentials to use in
    *                    authenticating this username
    */
   public Principal authenticate(String username, String credentials)
   {
      if (trace)
         log.trace("Begin authenticate, username=" + username);
      Principal principal = null;
      Context securityCtx = getSecurityContext();
      if (securityCtx == null)
      {
         if (trace)
            log.trace("No security context for authenticate(String, String)");
         return null;
      }

      Principal caller = (Principal) SecurityAssociationValve.userPrincipal.get();
      if (caller == null && username == null && credentials == null)
         return null;

      try
      {
         // Get the JBoss security manager from the ENC context
         SubjectSecurityManager securityMgr = (SubjectSecurityManager) securityCtx.lookup("securityMgr");
         Subject subject = new Subject();
         principal = new SimplePrincipal(username);
         if (securityMgr.isValid(principal, credentials, subject))
         {
            log.trace("User: " + username + " is authenticated");
            SecurityAssociationActions.setPrincipalInfo(principal, credentials, subject);
            // Get the CallerPrincipal mapping
            RealmMapping realmMapping = (RealmMapping) securityCtx.lookup("realmMapping");
            Principal oldPrincipal = principal;
            principal = realmMapping.getPrincipal(oldPrincipal);
            if (trace)
            {
               log.trace("Mapped from input principal: " + oldPrincipal
                  + "to: " + principal);
            }
            if (principal.equals(oldPrincipal) == false)
            {
               userPrincipalMap.put(principal, oldPrincipal);
            }
            // Get the caching principal
            principal = getCachingPrincpal(realmMapping, oldPrincipal, principal,
               credentials, subject);
         }
         else
         {
            principal = null;
            if (trace)
               log.trace("User: " + username + " is NOT authenticated");
         }
      }
      catch (NamingException e)
      {
         principal = null;
         log.error("Error during authenticate", e);
      }
      if (trace)
         log.trace("End authenticate, principal=" + principal);
      return principal;
   }

   /**
    * Return the Principal associated with the specified username and
    * credentials, if there is one; otherwise return <code>null</code>.
    * 
    * @param username    Username of the Principal to look up
    * @param credentials Password or other credentials to use in
    *                    authenticating this username
    */
   public Principal authenticate(String username, byte[] credentials)
   {
      return authenticate(username, new String(credentials));
   }

   /**
    * Return a short name for this Realm implementation, for use in
    * log messages.
    */
   protected String getName()
   {
      return getClass().getName();
   }

   /**
    * Return the password associated with the given principal's user name.
    */
   protected String getPassword(String username)
   {
      String password = null;
      return password;
   }

   /**
    * Return the Principal associated with the given user name.
    */
   protected Principal getPrincipal(String username)
   {
      return new SimplePrincipal(username);
   }

   /** Create the session principal tomcat will cache to avoid callouts to
    * this Realm.
    * 
    * @param realmMapping - the role mapping security manager
    * @param authPrincipal - the principal used for authentication and stored
    *    in the security manager cache
    * @param callerPrincipal - the possibly different caller principal
    *    representation of the authenticated principal
    * @param credential - the credential used for authentication
    * @return the tomcat session principal wrapper
    */ 
   protected Principal getCachingPrincpal(RealmMapping realmMapping,
      Principal authPrincipal, Principal callerPrincipal, Object credential,
      Subject subject)
   {
      // Cache the user roles in the principal
      Set userRoles = realmMapping.getUserRoles(authPrincipal);
      ArrayList roles = new ArrayList();
      if( userRoles != null )
      {
         Iterator iterator = userRoles.iterator();
         while (iterator.hasNext())
         {
            Principal role = (Principal) iterator.next();
            roles.add(role.getName());
         }
      }
      JBossGenericPrincipal gp = new JBossGenericPrincipal(this, subject,
         authPrincipal, callerPrincipal, credential, roles);
      return gp;
   }
}
