/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, 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.security.authentication;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.security.Principal;
import java.security.acl.Group;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;

import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import org.jboss.security.AuthenticationManager;
import org.jboss.security.CacheableManager;
import org.jboss.security.PicketBoxLogger;
import org.jboss.security.PicketBoxMessages;
import org.jboss.security.SecurityConstants;
import org.jboss.security.SecurityContext;
import org.jboss.security.SecurityContextAssociation;
import org.jboss.security.auth.callback.JBossCallbackHandler;
import org.jboss.security.auth.login.BaseAuthenticationInfo;
import org.jboss.security.authentication.JBossCachedAuthenticationManager.DomainInfo;
import org.jboss.security.config.ApplicationPolicy;
import org.jboss.security.config.SecurityConfiguration;
import org.jboss.security.plugins.ClassLoaderLocator;
import org.jboss.security.plugins.ClassLoaderLocatorFactory;

/**
 * {@link AuthenticationManager} implementation that uses {@code CacheableManager} as the cache provider.
 * 
 * @author <a href="mmoyses@redhat.com">Marcus Moyses</a>
 * @author <a href="on@ibis.odessa.ua">Oleg Nitz</a>
 * @author Scott.Stark@jboss.org
 * @author Anil.Saldhana@jboss.org
 */
public class JBossCachedAuthenticationManager implements AuthenticationManager, CacheableManager<ConcurrentMap<Principal, DomainInfo>, Principal>
{

   private String securityDomain;

   private CallbackHandler callbackHandler;

   private transient Method setSecurityInfo;

   protected ConcurrentMap<Principal, DomainInfo> domainCache;

   private boolean deepCopySubjectOption = false;

   /**
    * Create a new JBossCachedAuthenticationManager using the
    * default security domain and {@link CallbackHandler} implementation.
    */
   public JBossCachedAuthenticationManager()
   {
      this(SecurityConstants.DEFAULT_APPLICATION_POLICY, new JBossCallbackHandler());
   }

   /**
    * Create a new JBossCachedAuthenticationManager.
    * 
    * @param securityDomain name of the security domain
    * @param callbackHandler {@link CallbackHandler} implementation
    */
   public JBossCachedAuthenticationManager(String securityDomain, CallbackHandler callbackHandler)
   {
      this.securityDomain = securityDomain;
      this.callbackHandler = callbackHandler;

      // Get the setSecurityInfo(Principal principal, Object credential) method
      Class<?>[] sig = {Principal.class, Object.class};
      try
      {
         setSecurityInfo = callbackHandler.getClass().getMethod("setSecurityInfo", sig);
      }
      catch (Exception e)
      {
         throw new UnsupportedOperationException(PicketBoxMessages.MESSAGES.unableToFindSetSecurityInfoMessage());
      }
   }

   public Subject getActiveSubject()
   {
      Subject subj = null;
      SecurityContext sc = SecurityContextAssociation.getSecurityContext();
      if (sc != null)
      {
         subj = sc.getUtil().getSubject();
      }
      return subj;
   }

   public Principal getTargetPrincipal(Principal anotherDomainPrincipal, Map<String, Object> contextMap)
   {
      throw new UnsupportedOperationException();
   }

   public boolean isValid(Principal principal, Object credential)
   {
      return isValid(principal, credential, null);
   }

   public boolean isValid(Principal principal, Object credential, Subject activeSubject)
   {
      // first check cache
      DomainInfo cachedEntry = getCacheInfo(principal != null ? principal : new org.jboss.security.SimplePrincipal("null"));
      if (PicketBoxLogger.LOGGER.isTraceEnabled())
      {
         PicketBoxLogger.LOGGER.traceBeginIsValid(principal, cachedEntry != null ? cachedEntry.toString() : null);
      }

      boolean isValid = false;
      if (cachedEntry != null)
      {
         isValid = validateCache(cachedEntry, credential, activeSubject);
      }
      if (!isValid)
         isValid = authenticate(principal, credential, activeSubject);

      PicketBoxLogger.LOGGER.traceEndIsValid(isValid);
      return isValid;
   }

   public String getSecurityDomain()
   {
      return securityDomain;
   }

   public void flushCache()
   {
      PicketBoxLogger.LOGGER.traceFlushWholeCache();
      if (domainCache != null) {
         for (Principal principal : domainCache.keySet()) {
            this.flushCache(principal);
         }
      }
   }

   public void flushCache(Principal key)
   {
      if (key != null && this.domainCache != null && this.domainCache.containsKey(key))
         // this is currently done to preserve backwards compatibility - logout removes the entry from the cache and performs
         // the JAAS logout.
         this.logout(key, null);
   }

   public void setCache(ConcurrentMap<Principal, DomainInfo> cache)
   {
      this.domainCache = cache;
   }

   public boolean containsKey(Principal key)
   {
      if (domainCache != null && key != null)
         return domainCache.containsKey(key);
      return false;
   }
   
   public Set<Principal> getCachedKeys()
   {
      if (domainCache != null)
         return domainCache.keySet();
      return null;
   }

   /**
    * Flag to specify if deep copy of subject sets needs to be 
    * enabled
    * 
    * @param flag
    */
   public void setDeepCopySubjectOption(Boolean flag)
   {
      deepCopySubjectOption = flag.booleanValue();
   }

   /**
    * Retrieve on entry from the cache.
    * 
    * @param principal entry's key
    * @return entry's value or null if not found
    */
   private DomainInfo getCacheInfo(Principal principal)
   {
      if (domainCache != null && principal != null)
         return domainCache.get(principal);
      return null;
   }

   /**
    * Validate the cache credential value against the provided credential
    */
   @SuppressWarnings({"rawtypes", "unchecked"})
   private boolean validateCache(DomainInfo info, Object credential, Subject theSubject)
   {

      if (PicketBoxLogger.LOGGER.isTraceEnabled())
      {
         PicketBoxLogger.LOGGER.traceBeginValidateCache(info.toString(), credential != null ? credential.getClass() : null);
      }

      Object subjectCredential = info.credential;
      boolean isValid = false;
      // Check for a null credential as can be the case for an anonymous user
      if (credential == null || subjectCredential == null)
      {
         // Both credentials must be null
         isValid = (credential == null) && (subjectCredential == null);
      }
      // See if the credential is assignable to the cache value
      else if (subjectCredential.getClass().isAssignableFrom(credential.getClass()))
      {
         // Validate the credential by trying Comparable, char[], byte[], Object[], and finally Object.equals()
         if (subjectCredential instanceof Comparable)
         {
            Comparable c = (Comparable) subjectCredential;
            isValid = c.compareTo(credential) == 0;
         }
         else if (subjectCredential instanceof char[])
         {
            char[] a1 = (char[]) subjectCredential;
            char[] a2 = (char[]) credential;
            isValid = Arrays.equals(a1, a2);
         }
         else if (subjectCredential instanceof byte[])
         {
            byte[] a1 = (byte[]) subjectCredential;
            byte[] a2 = (byte[]) credential;
            isValid = Arrays.equals(a1, a2);
         }
         else if (subjectCredential.getClass().isArray())
         {
            Object[] a1 = (Object[]) subjectCredential;
            Object[] a2 = (Object[]) credential;
            isValid = Arrays.equals(a1, a2);
         }
         else
         {
            isValid = subjectCredential.equals(credential);
         }
      }
      else if (subjectCredential instanceof char[] && credential instanceof String)
      {
         char[] a1 = (char[]) subjectCredential;
         char[] a2 = ((String) credential).toCharArray();
         isValid = Arrays.equals(a1, a2);
      }
      else if (subjectCredential instanceof String && credential instanceof char[])
      {
         char[] a1 = ((String) subjectCredential).toCharArray();
         char[] a2 = (char[]) credential;
         isValid = Arrays.equals(a1, a2);
      }

      // If the credentials match, set the thread's active Subject
      if (isValid)
      {
         // Copy the current subject into theSubject
         if (theSubject != null)
         {
            SubjectActions.copySubject(info.subject, theSubject, false, this.deepCopySubjectOption);
         }
      }
      PicketBoxLogger.LOGGER.traceEndValidteCache(isValid);
      return isValid;
   }

   /** 
    * Currently this simply calls defaultLogin() to do a JAAS login using the
    * security domain name as the login module configuration name.
    *
    * @param principal - the user id to authenticate
    * @param credential - an opaque credential.
    * @return false on failure, true on success.
    */
   private boolean authenticate(Principal principal, Object credential, Subject theSubject)
   { 
	   ApplicationPolicy theAppPolicy = SecurityConfiguration.getApplicationPolicy(securityDomain);
	   if(theAppPolicy != null)
	   {
		   BaseAuthenticationInfo authInfo = theAppPolicy.getAuthenticationInfo();
		   List<String> jbossModuleNames = authInfo.getJBossModuleNames();
		   if(!jbossModuleNames.isEmpty())
		   {
			   ClassLoader currentTccl = SubjectActions.getContextClassLoader();
			   ClassLoaderLocator theCLL = ClassLoaderLocatorFactory.get();
			   if(theCLL != null)
			   {
				   ClassLoader newTCCL = theCLL.get(jbossModuleNames);
				   if(newTCCL != null)
				   {
					   try
					   {
						   SubjectActions.setContextClassLoader(newTCCL);
						   return proceedWithJaasLogin(principal, credential, theSubject, newTCCL);
					   }
					   finally
					   {
						   SubjectActions.setContextClassLoader(currentTccl);
					   }
				   }
			   }
		   }
	   }
	   return proceedWithJaasLogin(principal, credential, theSubject, null);
   }
   

   private boolean proceedWithJaasLogin(Principal principal, Object credential, Subject theSubject, ClassLoader contextClassLoader)
   {
	   Subject subject = null;
	   boolean authenticated = false;
	   LoginException authException = null;
	   try 
	   {
		   // Validate the principal using the login configuration for this domain
		   LoginContext lc = defaultLogin(principal, credential);
		   subject = lc.getSubject();

		   // Set the current subject if login was successful
		   if (subject != null)
		   {
			   // Copy the current subject into theSubject
			   if (theSubject != null)
			   {
				   SubjectActions.copySubject(subject, theSubject, false, this.deepCopySubjectOption);
			   }
			   else
			   {
				   theSubject = subject;
			   }

			   authenticated = true;
			   // Build the Subject based DomainInfo cache value
			   updateCache(lc, subject, principal, credential, contextClassLoader);
		   }
	   }
	   catch (LoginException e)
	   {
		   PicketBoxLogger.LOGGER.debugFailedLogin(e);
		   authException = e;
	   }
	   // Set the security association thread context info exception
	   SubjectActions.setContextInfo("org.jboss.security.exception", authException);

	   return authenticated;
   }

   /** 
    * Pass the security info to the login modules configured for
    * this security domain using our SecurityAssociationHandler.
    *
    * @return The authenticated Subject if successful.
    * @exception LoginException throw if login fails for any reason.
    */
   private LoginContext defaultLogin(Principal principal, Object credential) throws LoginException
   {
      // We use our internal CallbackHandler to provide the security info. A
      // copy must be made to ensure there is a unique handler per active
      // login since there can be multiple active logins.
      Object[] securityInfo = {principal, credential};
      CallbackHandler theHandler = null;
      try
      {
         theHandler = (CallbackHandler) callbackHandler.getClass().newInstance();
         setSecurityInfo.invoke(theHandler, securityInfo);
      }
      catch (Throwable e)
      {
         LoginException le = new LoginException(PicketBoxMessages.MESSAGES.unableToFindSetSecurityInfoMessage());
         le.initCause(e);
         throw le;
      }
      Subject subject = new Subject();
      LoginContext lc = null;
      PicketBoxLogger.LOGGER.traceDefaultLoginPrincipal(principal);
      lc = SubjectActions.createLoginContext(securityDomain, subject, theHandler);
      lc.login();
      if (PicketBoxLogger.LOGGER.isTraceEnabled())
      {
         PicketBoxLogger.LOGGER.traceDefaultLoginSubject(lc.toString(), SubjectActions.toString(subject));
      }
      return lc;
   }

   /**
    * Updates the cache either by inserting a new entry or by replacing
    * an invalid (expired) entry.
    * 
    * @param loginContext {@link LoginContext} of the authentication
    * @param subject {@link Subject} resulted from JAAS login
    * @param principal {@link Principal} representing the user's identity
    * @param credential user's proof of identity
    * @return authenticated {@link Subject}
    */
   private Subject updateCache(LoginContext loginContext, Subject subject, Principal principal, Object credential, ClassLoader lcClassLoader)
   {
      // If we don't have a cache there is nothing to update
      if (domainCache == null)
         return subject;

      DomainInfo info = new DomainInfo();
      info.loginContext = loginContext;
      info.subject = new Subject();
      SubjectActions.copySubject(subject, info.subject, true, this.deepCopySubjectOption);
      info.credential = credential;
      if (lcClassLoader == null) 
      {
            lcClassLoader = java.security.AccessController.doPrivileged(new java.security.PrivilegedAction<ClassLoader>() {
                public ClassLoader run() {
                    ClassLoader loader = Thread.currentThread().getContextClassLoader();
                    if (loader == null) 
                    {
                        loader = ClassLoader.getSystemClassLoader();
                    }
                    return loader;
                }
            });
       }
        info.contextClassLoader = lcClassLoader;
      if (PicketBoxLogger.LOGGER.isTraceEnabled())
      {
         PicketBoxLogger.LOGGER.traceUpdateCache(SubjectActions.toString(subject), SubjectActions.toString(info.subject));
      }

      // Get the Subject callerPrincipal by looking for a Group called 'CallerPrincipal'
      Set<Group> subjectGroups = subject.getPrincipals(Group.class);
      Iterator<Group> iter = subjectGroups.iterator();
      while (iter.hasNext())
      {
         Group grp = iter.next();
         String name = grp.getName();
         if (name.equals("CallerPrincipal"))
         {
            Enumeration<? extends Principal> members = grp.members();
            if (members.hasMoreElements())
               info.callerPrincipal = members.nextElement();
         }
      }

      // Handle null principals with no callerPrincipal. This is an indication
      // of an user that has not provided any authentication info, but
      // has been authenticated by the domain login module stack. Here we look
      // for the first non-Group Principal and use that.
      if (info.callerPrincipal == null)
      {
         Set<Principal> subjectPrincipals = subject.getPrincipals(Principal.class);
         Iterator<? extends Principal> iterPrincipals = subjectPrincipals.iterator();
         while (iterPrincipals.hasNext())
         {
            Principal p = iterPrincipals.next();
            if (!(p instanceof Group))
            {
               info.callerPrincipal = p;
               break;
            }
         }
      }

      // If the user already exists another login is active. Currently
      // only one is allowed so remove the old and insert the new
      domainCache.put(principal != null ? principal : new org.jboss.security.SimplePrincipal("null"), info);
      if (PicketBoxLogger.LOGGER.isTraceEnabled())
      {
         PicketBoxLogger.LOGGER.traceInsertedCacheInfo(info.toString());
      }

      return info.subject;
   }
    /**
     * Release cache entries got the specified ClassLoader.
     *
     * @param classLoader the ClassLoader.
     */
    public void releaseModuleEntries(final ClassLoader classLoader) {
        if (domainCache != null) {
            for (Entry<Principal, DomainInfo> entry : domainCache.entrySet()) {
                if ((classLoader == null && entry.getValue().contextClassLoader == null) || classLoader.equals(entry.getValue().contextClassLoader)) {
                    flushCache(entry.getKey());
                }
            }
        }
    }

   /**
    * A cache value. Holds information about the authentication process.
    * 
    * @author <a href="mmoyses@redhat.com">Marcus Moyses</a>
    */
   public static class DomainInfo implements Serializable
   {
      private static final long serialVersionUID = 7402775370244483773L;

      protected LoginContext loginContext;

      protected Subject subject;

      protected Object credential;

      protected Principal callerPrincipal;

      protected ClassLoader contextClassLoader = null;

      @Deprecated
      public void logout()
      {
         if (loginContext != null)
         {
            try
            {
               loginContext.logout();
            }
            catch (Exception e)
            {
               PicketBoxLogger.LOGGER.traceCacheEntryLogoutFailure(e);
            }
         }
      }
   }

   public void logout(Principal principal, Subject subject) {
       LoginContext context = null;

       // if a cache is active, remove the principal from the cache and try to perform the logout using the cached context.
       if (domainCache != null && principal != null) {
           if (PicketBoxLogger.LOGGER.isTraceEnabled())
           {
              PicketBoxLogger.LOGGER.traceFlushCacheEntry(principal.getName());
           }
           DomainInfo info = domainCache.get(principal);
           domainCache.remove(principal);
           if (info != null && info.loginContext != null) {
               context = info.loginContext;
               subject = info.subject;
           }
       }

       // if no cached context was found, create a new one with the incoming subject.
       if (context == null) {
          Object[] securityInfo = {principal, null};
          CallbackHandler theHandler = null;
          if (subject == null)
              subject = new Subject();
          try
          {
             theHandler = callbackHandler.getClass().newInstance();
             setSecurityInfo.invoke(theHandler, securityInfo);
             context = SubjectActions.createLoginContext(securityDomain, subject, theHandler);
          }
          catch (Throwable e)
          {
             LoginException le = new LoginException(PicketBoxMessages.MESSAGES.unableToInitializeLoginContext(e));
             le.initCause(e);
             SubjectActions.setContextInfo("org.jboss.security.exception", le);
             return;
          }
       }

       // perform the JAAS logout.
       try {
           context.logout();
           if (PicketBoxLogger.LOGGER.isTraceEnabled())
           {
              PicketBoxLogger.LOGGER.traceLogoutSubject(context.toString(), SubjectActions.toString(subject));
           }
       }
       catch (LoginException le) {
           SubjectActions.setContextInfo("org.jboss.security.exception", le);
       }

   }
}
