/*
 * 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.security.integration;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.security.auth.callback.CallbackHandler;

import org.jboss.logging.Logger;
import org.jboss.managed.api.annotation.ManagementComponent;
import org.jboss.managed.api.annotation.ManagementObject;
import org.jboss.managed.api.annotation.ManagementOperation;
import org.jboss.managed.api.annotation.ManagementParameter;
import org.jboss.managed.api.annotation.ManagementProperties;
import org.jboss.managed.api.annotation.ManagementProperty;
import org.jboss.managed.api.annotation.ViewUse;
import org.jboss.security.AuthenticationManager;
import org.jboss.security.AuthorizationManager;
import org.jboss.security.ISecurityManagement;
import org.jboss.security.SecurityConstants;
import org.jboss.security.audit.AuditManager;
import org.jboss.security.auth.AuthenticationCacheFlushThread;
import org.jboss.security.auth.AuthenticationTimedCachePolicy;
import org.jboss.security.auth.callback.JBossCallbackHandler;
import org.jboss.security.config.SecurityConfiguration;
import org.jboss.security.identitytrust.IdentityTrustManager;
import org.jboss.security.mapping.MappingManager;
import org.jboss.security.plugins.JaasSecurityDomain;
import org.jboss.security.plugins.SecurityDomainContext;
import org.jboss.util.CachePolicy;
import org.jboss.util.TimedCachePolicy;
 
/**
 *  JNDI Based Security Management
 *  @author Anil.Saldhana@redhat.com
 *  @since  Sep 9, 2007 
 *  @version $Revision: 97550 $
 */ 
@ManagementObject(name="JNDIBasedSecurityManagement", componentType = @ManagementComponent(type = "MCBean", subtype = "Security"),
                  properties = ManagementProperties.EXPLICIT)
public class JNDIBasedSecurityManagement implements ISecurityManagement
{ 
   private static final long serialVersionUID = 1L;

   public static final String CBH = "org.jboss.security.callbackhandler";
   
   protected static Logger log = Logger.getLogger(JNDIBasedSecurityManagement.class);
   
   static transient ConcurrentHashMap<String,SecurityDomainContext> securityMgrMap = new ConcurrentHashMap<String,SecurityDomainContext>();
    
   protected String BASE_CTX = SecurityConstants.JAAS_CONTEXT_ROOT; 
   
   protected String authenticationMgrClass = "org.jboss.security.plugins.JaasSecurityManager";
   
   protected String authorizationMgrClass = "org.jboss.security.plugins.JBossAuthorizationManager";
   
   protected String auditMgrClass = "org.jboss.security.plugins.audit.JBossAuditManager";
   
   protected String identityTrustMgrClass = "org.jboss.security.plugins.identitytrust.JBossIdentityTrustManager";
   
   protected String mappingMgrClass = "org.jboss.security.plugins.mapping.JBossMappingManager";
   
   protected static transient CallbackHandler callBackHandler = new JBossCallbackHandler(); 
   
   /** Enable the IdentityTrust feature */
   protected boolean enableIdentity = false;
   
   /** Enable the Audit feature */
   protected boolean enableAudit = true; 
   
   private CachePolicy cachePolicy = null;
   
   private transient ConcurrentHashMap<String,AuthenticationManager> authMgrMap = null;
   private transient ConcurrentHashMap<String,AuthorizationManager> authzMgrMap = null;
   private transient ConcurrentHashMap<String,MappingManager> mappingMgrMap = null;
   private transient ConcurrentHashMap<String,AuditManager> auditMgrMap = null;
   private transient ConcurrentHashMap<String,IdentityTrustManager> idmMgrMap = null;
   
   /** Thread to cleanup the authentication cache */
   private static AuthenticationCacheFlushThread authCacheFlushThread;
   
   public JNDIBasedSecurityManagement()
   {   
      initialize();
      initializeCallbackHandler();
   } 
   
   @ManagementOperation(description = "Get the audit manager for the specified security domain",
         params = {@ManagementParameter(name = "securityDomain", description = "The security domain name")})
   public AuditManager getAuditManager(String securityDomain)
   { 
      initialize();
      AuditManager auditManager = null;
      try
      { 
         if(this.enableAudit)
         {
            auditManager = this.auditMgrMap.get(securityDomain);
            if(auditManager == null)
            {
               auditManager = (AuditManager) lookUpJNDI(securityDomain + "/auditMgr");
               this.auditMgrMap.put(securityDomain, auditManager); 
            } 
         }  
      }
      catch(Exception e)
      {
         log.trace("Exception in getting audit mgr", e); 
      }
      return auditManager;
   }

   @ManagementOperation(description = "Get the authentication manager for the specified security domain",
         params = {@ManagementParameter(name = "securityDomain", description = "The security domain name")})
   public AuthenticationManager getAuthenticationManager(String securityDomain)
   {
      initialize();
      AuthenticationManager am = null;
      try
      {
         am = this.authMgrMap.get(securityDomain);
         if(am == null)
         {
            am = (AuthenticationManager) lookUpJNDI(securityDomain + "/authenticationMgr");
            this.authMgrMap.put(securityDomain, am); 
         }
      }
      catch(Exception e)
      {
         log.trace("Exception in getting authentication mgr "
               + " for domain="+securityDomain , e );
      }
      return am;
   }

   @ManagementOperation(description = "Get the authorization manager for the specified security domain",
         params = {@ManagementParameter(name = "securityDomain", description = "The security domain name")})
   public AuthorizationManager getAuthorizationManager(String securityDomain)
   {
      initialize();
      AuthorizationManager am = null;
      try
      {
         am = this.authzMgrMap.get(securityDomain);
         if(am == null)
         {
            am = (AuthorizationManager) lookUpJNDI(securityDomain + "/authorizationMgr");
            this.authzMgrMap.put(securityDomain, am);
         }
      }
      catch(Exception e)
      {
         log.trace("Exception in getting authorization mgr", e);
      }
      return am;
   }

   @ManagementOperation(description = "Get the identity trust manager for the specified security domain",
         params = {@ManagementParameter(name = "securityDomain", description = "The security domain name")})
   public IdentityTrustManager getIdentityTrustManager(String securityDomain)
   {
      initialize();
      IdentityTrustManager am = null;
      try
      {
         if(this.enableIdentity)
         {
            am = this.idmMgrMap.get(securityDomain);
            if(am == null)
            {
               am = (IdentityTrustManager) lookUpJNDI(securityDomain + "/identityTrustMgr");
               this.idmMgrMap.put(securityDomain, am); 
            } 
         }
      }
      catch(Exception e)
      {
         log.trace("Exception in getting IdentityTrustManager", e);
      }
      return am;
   }

   @ManagementOperation(description = "Get the mapping manager for the specified security domain",
         params = {@ManagementParameter(name = "securityDomain", description = "The security domain name")})
   public MappingManager getMappingManager(String securityDomain)
   {
      initialize();
      MappingManager am = null;
      try
      {
         am = this.mappingMgrMap.get(securityDomain);
         if(am == null)
         {
            am = (MappingManager) lookUpJNDI(securityDomain + "/mappingMgr");
            this.mappingMgrMap.put(securityDomain, am); 
         }
      }
      catch(Exception e)
      {
         log.trace("Exception in getting MappingManager", e);
      }
      return am;
   }
       
   @ManagementProperty(use = {ViewUse.CONFIGURATION}, 
         description = "The class that implements the AuthenticationManager interface")
   public void setAuthenticationMgrClass(String authenticationMgrClass)
   {
      this.authenticationMgrClass = authenticationMgrClass;
   }

   @ManagementProperty(use = {ViewUse.CONFIGURATION}, 
         description = "The class that implements the AuthorizationManager interface")
   public void setAuthorizationMgrClass(String authorizationMgrClass)
   {
      this.authorizationMgrClass = authorizationMgrClass;
   }

   @ManagementProperty(use = {ViewUse.CONFIGURATION}, 
         description = "The class that implements the AuditManager interface")
   public void setAuditMgrClass(String auditMgrClass)
   {
      this.auditMgrClass = auditMgrClass;
   } 

   @ManagementProperty(use = {ViewUse.CONFIGURATION}, 
         description = "The class that implements the IdentityTrustManager interface")
   public void setIdentityTrustMgrClass(String identityTrustMgrClass)
   {
      this.identityTrustMgrClass = identityTrustMgrClass;
   }

   @ManagementProperty(use = {ViewUse.CONFIGURATION}, 
         description = "The class that implements the MappingManager interface")
   public void setMappingMgrClass(String mappingMgrClass)
   {
      this.mappingMgrClass = mappingMgrClass;
   }

   public void setCallBackHandler(CallbackHandler cbh)
   {
      callBackHandler = cbh;
      securityMgrMap.clear();
   }

   public void setEnableAudit(boolean enableAudit)
   {
      this.enableAudit = enableAudit;
   }
   
   public void setEnableIdentity(boolean enableIdentity)
   {
      this.enableIdentity = enableIdentity;
   }

   public void setCachePolicy(CachePolicy cp)
   {
      this.cachePolicy = cp;
   } 
   
   public void setBaseContext(String ctx)
   {
      if(ctx == null)
         throw new IllegalArgumentException("ctx is null");
      this.BASE_CTX = ctx;
   }
     
   /** Set the indicated security domain cache timeout. This only has an
   effect if the security domain is using the default jboss TimedCachePolicy
   implementation.

   @param securityDomain the name of the security domain cache
   @param timeoutInSecs - the cache timeout in seconds.
   @param resInSecs - resolution of timeouts in seconds.
   */
  public static void setCacheTimeout(String securityDomain, int timeoutInSecs, int resInSecs)
  {
     SecurityDomainContext securityDomainCtx = (SecurityDomainContext) securityMgrMap.get(securityDomain);
     if(securityDomainCtx == null)
     {
      try
      {
         String lookupStr = SecurityConstants.JAAS_CONTEXT_ROOT + "/" + securityDomain;
         securityDomainCtx = (SecurityDomainContext) new InitialContext().lookup(lookupStr);
         securityMgrMap.put(securityDomain, securityDomainCtx);
      }
      catch (NamingException e)
      {
         log.trace("SetCacheTimeOut:Failed to look up SecurityDomainCtx:"+securityDomain);
      }  
     }
     if(securityDomainCtx != null)
     {
        CachePolicy cache = securityDomainCtx.getAuthenticationCache(); 
        if( cache != null && cache instanceof TimedCachePolicy )
        {
           TimedCachePolicy tcp = (TimedCachePolicy) cache;
           synchronized( tcp )
           {
              tcp.setDefaultLifetime(timeoutInSecs);
              tcp.setResolution(resInSecs);
           }
        }
        else
        {
           log.warn("Failed to find cache policy for securityDomain='"
              + securityDomain + "'");
        } 
     }
  } 
   
   public static void setDefaultCacheTimeout(int defaultCacheTimeout)
   {
      SecurityConstantsBridge.defaultCacheTimeout = defaultCacheTimeout;
   }

   public static void setDefaultCacheResolution(int defaultCacheResolution)
   {
      SecurityConstantsBridge.defaultCacheResolution = defaultCacheResolution;
   }
   
   public static void setDefaultCacheFlushPeriod(int flushPeriodInSecs)
   {
      SecurityConstantsBridge.defaultCacheFlushPeriod = flushPeriodInSecs;
      if (SecurityConstantsBridge.defaultCacheFlushPeriod == 0 && authCacheFlushThread != null)
      {
         authCacheFlushThread.interrupt();
         authCacheFlushThread = null;
      }
      if (SecurityConstantsBridge.defaultCacheFlushPeriod > 0 && authCacheFlushThread == null)
      {
         authCacheFlushThread = new AuthenticationCacheFlushThread(securityMgrMap);
         authCacheFlushThread.start();
      }
   }

   @ManagementOperation(description = "Create the context for the specified security domain",
         params = {@ManagementParameter(name = "securityDomain", description = "The security domain name")})
   public SecurityDomainContext createSecurityDomainContext(String securityDomain) throws Exception
   {   
      log.debug("Creating SDC for domain="+securityDomain);
      AuthenticationManager am = createAuthenticationManager(securityDomain);
      if(cachePolicy == null)
      {
         cachePolicy = createDefaultCachePolicy();
      }
      //Set security cache if the auth manager implementation supports it
      setSecurityDomainCache(am, cachePolicy);
      //Set DeepCopySubject option if supported
      if(SecurityConfiguration.isDeepCopySubjectMode())
      {
        setDeepCopySubjectMode(am);  
      }
      
      SecurityDomainContext securityDomainContext = new SecurityDomainContext(am, cachePolicy); 
      
      securityDomainContext.setAuthorizationManager(createAuthorizationManager(securityDomain));
      securityDomainContext.setAuditMgr(createAuditManager(securityDomain));
      securityDomainContext.setIdentityTrustMgr(createIdentityTrustManager(securityDomain));
      securityDomainContext.setMappingMgr(createMappingManager(securityDomain));
      return securityDomainContext;
   }
   
   /**
    * Legacy registration of JaasSecurityDomain instance with the JNDI
    * Object Factory internal hashmap
    * @param domain
    * @param jsd
    * @throws Exception
    */
   @ManagementOperation(description = "Register the specified security domain",
         params = {@ManagementParameter(name = "domain", description = "The security domain being registered")})
   public void registerJaasSecurityDomainInstance(JaasSecurityDomain domain) throws Exception
   {
      String domainName = domain.getSecurityDomain();
      SecurityDomainContext sdc = (SecurityDomainContext) securityMgrMap.get(domainName);
      if(sdc != null)
      {
         sdc.setAuthenticationManager(domain);
      }
      else
      {
         sdc = createSecurityDomainContext(domainName);
         sdc.setAuthenticationManager(domain);
      }
      securityMgrMap.put(domainName, sdc);
   }  
   
   /**
    * Legacy deregistration of JaasSecurityDomain instance with the JNDI
    * Object Factory internal hashmap
    * @param securityDomain
    * @param jsd
    * @throws Exception
    */
   @ManagementOperation(description = "Deregister the specified security domain",
         params = {@ManagementParameter(name = "securityDomain", description = "The name of the security domain being deregistered")})
   public void deregisterJaasSecurityDomainInstance(String securityDomain)
   {
      securityMgrMap.remove(securityDomain); 
   }  
   
   /**
    * Clear all the maps
    */
   public static void clear()
   {
      RuntimePermission rtp = new RuntimePermission(JNDIBasedSecurityManagement.class.getName());
      SecurityManager sm = System.getSecurityManager();
      if(sm != null)
         sm.checkPermission(rtp);
      
      securityMgrMap.clear();
   }

   // Private Methods
 
   private Object lookUpJNDI(String ctxName) 
   {
      Object result = null;
      try
      { 
         Context ctx = new InitialContext();
         if(ctxName.startsWith(BASE_CTX))
            result = ctx.lookup(ctxName);
         else
            result = ctx.lookup(BASE_CTX + "/" + ctxName);  
      }
      catch(Exception e)
      {
         log.trace("Look up of JNDI for " + ctxName + " failed with "+ e.getLocalizedMessage());
         return null;
      }
      return result;
   }
   
   private AuthenticationManager createAuthenticationManager(String securityDomain) throws Exception
   {
      Class<?> clazz = SecurityActions.getContextClassLoader().loadClass(authenticationMgrClass);
      Constructor<?> ctr = clazz.getConstructor(new Class[] { String.class, CallbackHandler.class});
      return (AuthenticationManager) ctr.newInstance(new Object[]{ securityDomain, callBackHandler});
   }
   
   private AuthorizationManager createAuthorizationManager(String securityDomain) throws Exception
   {
      Class<?> clazz = SecurityActions.getContextClassLoader().loadClass(authorizationMgrClass);
      Constructor<?> ctr = clazz.getConstructor(new Class[] { String.class});
      return (AuthorizationManager) ctr.newInstance(new Object[]{ securityDomain});
   }
   
   private AuditManager createAuditManager(String securityDomain) throws Exception
   {
      Class<?> clazz = SecurityActions.getContextClassLoader().loadClass(auditMgrClass);
      Constructor<?> ctr = clazz.getConstructor(new Class[] { String.class});
      return (AuditManager) ctr.newInstance(new Object[]{ securityDomain});
   }
   
   private MappingManager createMappingManager(String securityDomain) throws Exception
   {
      Class<?> clazz = SecurityActions.getContextClassLoader().loadClass(mappingMgrClass);
      Constructor<?> ctr = clazz.getConstructor(new Class[] { String.class});
      return (MappingManager) ctr.newInstance(new Object[]{ securityDomain});
   }
   
   private IdentityTrustManager createIdentityTrustManager(String securityDomain) throws Exception
   {
      Class<?> clazz = SecurityActions.getContextClassLoader().loadClass(identityTrustMgrClass);
      Constructor<?> ctr = clazz.getConstructor(new Class[] { String.class});
      return (IdentityTrustManager) ctr.newInstance(new Object[]{ securityDomain});
   }
   
   /** Use reflection to attempt to set the authentication cache on the
    * securityMgr argument.
    * @param securityMgr the security manager
    * @param cachePolicy the cache policy implementation
    */
   private static void setSecurityDomainCache(AuthenticationManager securityMgr,
      CachePolicy cachePolicy)
   {
      try
      {
         Class<?>[] setCachePolicyTypes = {CachePolicy.class};
         Method m = securityMgr.getClass().getMethod("setCachePolicy", setCachePolicyTypes);
         Object[] setCachePolicyArgs = {cachePolicy};
         m.invoke(securityMgr, setCachePolicyArgs);
         log.debug("setCachePolicy, c="+setCachePolicyArgs[0]);
      }
      catch(Exception e2)
      {    
         if(log.isTraceEnabled())
            log.trace("Optional setCachePolicy failed" + e2.getLocalizedMessage());
      }
   }
   
   /** Use reflection to attempt to set the authentication cache on the
    * securityMgr argument.
    * @param securityMgr the security manager
    * @param cachePolicy the cache policy implementation
    */
   private static void setDeepCopySubjectMode(AuthenticationManager securityMgr)
   {
      try
      {
         Class<?>[] argsType = {Boolean.class};
         Method m = securityMgr.getClass().getMethod("setDeepCopySubjectOption", argsType);
         Object[] deepCopyArgs = {Boolean.TRUE};
         m.invoke(securityMgr, deepCopyArgs);
         log.trace("setDeepCopySubjectOption, option="+deepCopyArgs[0]);
      }
      catch(Exception e2)
      {    
         if(log.isTraceEnabled())
            log.trace("Optional setDeepCopySubjectMode failed" + e2.getLocalizedMessage());
      }
   }
   
   /**
    * Create a Default Cache Policy
    * @return
    */
   private CachePolicy createDefaultCachePolicy()
   {
      TimedCachePolicy cachePolicy = 
          new AuthenticationTimedCachePolicy(SecurityConstantsBridge.defaultCacheTimeout,
                               true, 
                               SecurityConstantsBridge.defaultCacheResolution);
      cachePolicy.create();
      cachePolicy.start();
      return cachePolicy; 
   } 
   
   /**
    * Since the maps are transient, initialize them
    */
   private void initialize()
   {
      if(authMgrMap == null)
         authMgrMap = new ConcurrentHashMap<String,AuthenticationManager>();
      if(authzMgrMap == null)
         authzMgrMap = new ConcurrentHashMap<String,AuthorizationManager>();
      if(mappingMgrMap == null)
         mappingMgrMap = new ConcurrentHashMap<String,MappingManager>();
      if(auditMgrMap == null)
         auditMgrMap = new ConcurrentHashMap<String,AuditManager>();
      if(idmMgrMap == null)
         idmMgrMap = new ConcurrentHashMap<String,IdentityTrustManager>();
   }
   
   private void initializeCallbackHandler()
   {
	   //Look for a system property for a VM wide Callback Handler
	   String cbh = SecurityActions.getSystemProperty(CBH, null);
	   if(cbh != null)
	   {
		   try
		   { 
			   ClassLoader tcl = SecurityActions.getContextClassLoader();
			   Class<?> clazz = tcl.loadClass(cbh);
			   callBackHandler = (CallbackHandler) clazz.newInstance();
		   }
		   catch(Exception e)
		   {
			   throw new RuntimeException("Error initializing JNDIBasedSecurityManagement:",e);
		   }
	   }
	   if(callBackHandler == null)
		   callBackHandler = new JBossCallbackHandler(); 
   }

   public void start()
   {
      // start the authentication cache flush thread
      if (SecurityConstantsBridge.defaultCacheFlushPeriod > 0 && authCacheFlushThread == null)
      {
         authCacheFlushThread = new AuthenticationCacheFlushThread(securityMgrMap);
         authCacheFlushThread.start();
      }
   }

   public void stop()
   {
      if (authCacheFlushThread != null)
      {
         authCacheFlushThread.interrupt();
         authCacheFlushThread = null;
      }
   }
}