/*
  * 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.ejb3.test.security.unit;

import java.util.HashSet;
import javax.ejb.EJBException;
import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueReceiver;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.management.MBeanServerConnection;
import javax.management.MBeanServerInvocationHandler;
import javax.management.ObjectName;
import javax.naming.InitialContext;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import org.jboss.ejb3.test.security.AppCallbackHandler;
import org.jboss.ejb3.test.security.CalledSession;
import org.jboss.ejb3.test.security.SecurityContext;
import org.jboss.ejb3.test.security.SessionFacade;
import org.jboss.ejb3.test.security.StatefulSession;
import org.jboss.ejb3.test.security.StatelessSession;
import org.jboss.logging.Logger;
import org.jboss.security.SimplePrincipal;
import org.jboss.security.auth.login.XMLLoginConfigImpl;
import org.jboss.security.plugins.JaasSecurityManagerServiceMBean;
import org.jboss.test.JBossTestCase;
import junit.framework.Test;


/** Test of EJB spec conformace using the security-spec.jar
 deployment unit. These test the basic role based access model.
 
 @author Scott.Stark@jboss.org
 @version $Revision$
 */
public class EJBSpecUnitTestCase
   extends JBossTestCase
{
   private static final Logger log = Logger.getLogger(EJBSpecUnitTestCase.class);
   
   static String username = "scott";
   static char[] password = "echoman".toCharArray();
   static String QUEUE_FACTORY = "ConnectionFactory";
   
   LoginContext lc;
   boolean loggedIn;

   public EJBSpecUnitTestCase(String name)
   {
      super(name);
   }
   
   protected void tearDown() throws Exception
   {
      logout();
   }

   /** Validate that the users have the expected logins and roles.
    * 
    * @throws Exception
    */ 
   public void testSecurityDomain() throws Exception
   {
      log.info("+++ testSecurityDomain, domain=spec-test");
      MBeanServerConnection conn = (MBeanServerConnection) getServer();
      ObjectName secMgrName = new ObjectName("jboss.security:service=JaasSecurityManager");
      JaasSecurityManagerServiceMBean secMgr = (JaasSecurityManagerServiceMBean)
         MBeanServerInvocationHandler.newProxyInstance(conn, secMgrName,
         JaasSecurityManagerServiceMBean.class, false);

      // Test the spec-test security domain
      String domain = "spec-test";
      SimplePrincipal user = new SimplePrincipal("scott");
      boolean isValid = secMgr.isValid(domain, user, password);
      assertTrue("scott password is echoman", isValid);
      HashSet testRole = new HashSet();
      testRole.add(new SimplePrincipal("Echo"));
      boolean hasRole = secMgr.doesUserHaveRole(domain, user, password, testRole);
      assertTrue("scott has Echo role", hasRole);
      testRole.clear();
      testRole.add(new SimplePrincipal("EchoLocal"));
      hasRole = secMgr.doesUserHaveRole(domain, user, password, testRole);
      assertTrue("scott has EchoLocal role", hasRole);
      testRole.clear();
      testRole.add(new SimplePrincipal("ProjectUser"));
      hasRole = secMgr.doesUserHaveRole(domain, user, password, testRole);
      assertTrue("scott has ProjectUser role", hasRole);

      isValid = secMgr.isValid(domain, user, "badpass".toCharArray());
      assertTrue("badpass is an invalid password for scott", isValid == false);

      // Test the spec-test-domain security domain
      log.info("+++ testSecurityDomain, domain=spec-test-domain");
      domain = "spec-test-domain";
      isValid = secMgr.isValid(domain, user, password);
      assertTrue("scott password is echoman", isValid);
      hasRole = secMgr.doesUserHaveRole(domain, user, password, testRole);
      assertTrue("scott has Echo role", hasRole);
      testRole.clear();
      testRole.add(new SimplePrincipal("EchoLocal"));
      hasRole = secMgr.doesUserHaveRole(domain, user, password, testRole);
      assertTrue("scott has EchoLocal role", hasRole);
      testRole.clear();
      testRole.add(new SimplePrincipal("ProjectUser"));
      hasRole = secMgr.doesUserHaveRole(domain, user, password, testRole);
      assertTrue("scott has ProjectUser role", hasRole);      

      isValid = secMgr.isValid(domain, user, "badpass".toCharArray());
      assertTrue("badpass is an invalid password for scott", isValid == false);
   }

   /** Test that:
    1. SecureBean returns a non-null principal when getCallerPrincipal
    is called with a security context and that this is propagated
    to its Entity bean ref.
    
    2. UnsecureBean throws an IllegalStateException when getCallerPrincipal
    is called without a security context.
    */
   public void testGetCallerPrincipal() throws Exception
   {   
      log.debug("+++ testGetCallerPrincipal()");
      StatelessSession bean = (StatelessSession)getInitialContext().lookup("spec.UnsecureStatelessSession2");
      log.debug("Created spec.UnsecureStatelessSession2");
      
      try
      {
         // This should fail because echo calls getCallerPrincipal()
         bean.echo("Hello from nobody?");
         fail("Was able to call StatelessSession.echo");
      }
      catch(Exception e)
      {
         log.debug("echo failed as expected");
      }
      
      login();
      bean = (StatelessSession)getInitialContext().lookup("spec.StatelessSession2");
      log.debug("Created spec.StatelessSession2");
      // Test that the Entity bean sees username as its principal
      String echo = bean.echo(username);
      log.debug("bean.echo(username) = "+echo);
      assertTrue("username == echo", echo.equals(username));
      
      logout();
   }
   
   /** Test the use of getCallerPrincipal from within the ejbCreate
    * in a stateful session bean
    */
   public void testStatefulCreateCaller() throws Exception
   {
      log.debug("+++ testStatefulCreateCaller");
      login();
      InitialContext jndiContext = new InitialContext();
     
      StatefulSession bean = (StatefulSession)jndiContext.lookup("spec.StatefulSession");
      // Need to invoke a method to ensure an ejbCreate call
      bean.echo("testStatefulCreateCaller");
      log.debug("Bean.echo(), ok");

      logout();
   }

   /**
    * Test that a call interacting with different security domains does not
    * change the 
    * @throws Exception
    */ 
   public void testDomainInteraction() throws Exception
   {
      logout();
      login("testDomainInteraction", "testDomainInteraction".toCharArray());
      log.debug("+++ testDomainInteraction()");
      SecurityContext bean = (SecurityContext)getInitialContext().lookup("spec.UserInRoleContextSession");
      log.debug("Created spec.UserInRoleContextSession");
      HashSet roles = new HashSet();
      roles.add("Role1");
      roles.add("Role2");
      bean.testDomainInteraction(roles);   
   }

   /** Test that the calling principal is propagated across bean calls.
    */
   public void testPrincipalPropagation() throws Exception
   {
      log.debug("+++ testPrincipalPropagation");
      logout();
      login();
      StatelessSession bean = (StatelessSession)getInitialContext().lookup("spec.UnsecureStatelessSession2");
      log.debug("Created spec.UnsecureStatelessSession2");
      log.debug("Bean.forward('testPrincipalPropagation') -> "+bean.forward("testPrincipalPropagation"));
   }
   
   public void testMethodAccess() throws Exception
   {
      log.debug("+++ testMethodAccess");
      login();
      StatelessSession bean = (StatelessSession)getInitialContext().lookup("spec.StatelessSession");
      log.debug("Created spec.StatelessSession");
      log.debug("Bean.echo('Hello') -> "+bean.echo("Hello"));

      // This should be allowed in ejb3
      bean.noop();
   }

   /** Test that the echo method is accessible by an Echo
    role. Since the excluded() method of the StatelessSession
    bean has been placed into the excluded set it should not
    accessible by any user. This uses the security domain of the
    JaasSecurityDomain service to test its use as an authentication mgr.
    */
   public void testDomainMethodAccess() throws Exception
   {
      log.debug("+++ testDomainMethodAccess");
      login();
      StatelessSession bean = (StatelessSession)getInitialContext().lookup("spec.StatelessSessionInDomain");
      log.debug("Created spec.StatelessSessionInDomain");
      log.debug("Bean.echo('testDomainMethodAccess') -> "+bean.echo("testDomainMethodAccess"));

      try
      {
         // This should not be allowed
         bean.excluded();
         fail("Was able to call StatelessSession.excluded");
      }
      catch(Exception e)
      {
         log.debug("StatelessSession.excluded failed as expected");
      }
   }

   /** Test that the permissions assigned to the stateless session bean:
    with ejb-name=org/jboss/test/security/ejb/StatelessSession_test
    are read correctly.
    */
   public void testMethodAccess2() throws Exception
   {
      log.debug("+++ testMethodAccess2");
      login();
      InitialContext jndiContext = new InitialContext();
      StatelessSession bean = (StatelessSession)jndiContext.lookup("spec.StatelessSession_test");
      log.debug("Created spec.StatelessSession_test");
      log.debug("Bean.echo('testMethodAccess2') -> "+bean.echo("testMethodAccess2"));
   }

   /** Test a user with Echo and EchoLocal roles can access the CalleeBean
    through its local interface by calling the CallerBean and that a user
    with only a EchoLocal cannot call the CallerBean.
    */
   public void a1testLocalMethodAccess() throws Exception
   {
      log.debug("+++ testLocalMethodAccess");
      login();
      InitialContext jndiContext = new InitialContext();
      CalledSession bean = (CalledSession)jndiContext.lookup("spec.CallerBean");
      log.debug("Created spec.CallerBean");
      log.debug("Bean.invokeEcho('testLocalMethodAccess') -> "+bean.invokeEcho("testLocalMethodAccess"));
   }

   /** Test access to a bean with a mix of remote interface permissions and
    * unchecked permissions with the unchecked permissions declared first.
    * @throws Exception
    */ 
   public void testUncheckedRemote() throws Exception
   {
      log.debug("+++ testUncheckedRemote");
      login();
      StatelessSession bean = (StatelessSession)getInitialContext().lookup("spec.UncheckedSessionRemoteLast");
      log.debug("Created spec.UncheckedSessionRemoteLast");
      log.debug("Bean.echo('testUncheckedRemote') -> "+bean.echo("testUncheckedRemote"));
      try
      {
         bean.excluded();
         fail("Was able to call UncheckedSessionRemoteLast.excluded");
      }
      catch(Exception e)
      {
         log.debug("UncheckedSessionRemoteLast.excluded failed as expected");         
      }
      logout();
   }

   /** Test access to a bean with a mix of remote interface permissions and
    * unchecked permissions with the unchecked permissions declared last.
    * @throws Exception
    */ 
   public void testRemoteUnchecked() throws Exception
   {
      log.debug("+++ testRemoteUnchecked");
      login();
      StatelessSession bean = (StatelessSession)getInitialContext().lookup("spec.UncheckedSessionRemoteFirst");
      log.debug("Created spec.UncheckedSessionRemoteFirst");
      log.debug("Bean.echo('testRemoteUnchecked') -> "+bean.echo("testRemoteUnchecked"));
      try
      {
         bean.excluded();
         fail("Was able to call UncheckedSessionRemoteFirst.excluded");
      }
      catch(Exception e)
      {
         log.debug("UncheckedSessionRemoteFirst.excluded failed as expected");         
      }
      logout();
   }

   /** Test that a user with a role that has not been assigned any
    method permissions in the ejb-jar descriptor is able to access a
    method that has been marked as unchecked.
    */
   public void testUnchecked() throws Exception
   {
      log.debug("+++ testUnchecked");
      // Login as scott to create the bean
      login();
      StatelessSession bean = (StatelessSession)getInitialContext().lookup("spec.StatelessSession");
      log.debug("Created spec.StatelessSession");
      // Logout and login back in as stark to test access to the unchecked method
      logout();
      login("stark", "javaman".toCharArray());
      bean.unchecked();
      log.debug("Called Bean.unchecked()");
      logout();
   }

   /** Test that a user with a valid role is able to access a
    bean for which all methods have been marked as unchecked.
    */
   public void testUncheckedWithLogin() throws Exception
   {
      log.debug("+++ testUncheckedWithLogin");
      // Login as scott to see that a user with roles is allowed access
      login();
      StatelessSession bean = (StatelessSession)getInitialContext().lookup("spec.UncheckedSession");
      log.debug("Created spec.StatelessSession");
      bean.unchecked();
      log.debug("Called Bean.unchecked()");
      logout();
   }

   /** Test that user scott who has the Echo role is not able to
    access the StatelessSession2.excluded method even though
    the Echo role has been granted access to all methods of
    StatelessSession2 to test that the excluded-list takes
    precendence over the method-permissions.
    */
   public void testExcluded() throws Exception
   {
      log.debug("+++ testExcluded");
      login();
      StatelessSession bean = (StatelessSession)getInitialContext().lookup("spec.StatelessSession2");
      log.debug("Created spec.StatelessSession2");
      try
      {
         bean.excluded();
         fail("Was able to call Bean.excluded()");
      }
      catch(Exception e)
      {
         log.debug("Bean.excluded() failed as expected");
         // This is what we expect
      }
      logout();
   }
   
   /** This method tests the following call chains:
    1. RunAsStatelessSession.echo() -> PrivateEntity.echo()
    2. RunAsStatelessSession.noop() -> RunAsStatelessSession.excluded()
    3. RunAsStatelessSession.forward() -> StatelessSession.echo()
    1. Should succeed because the run-as identity of RunAsStatelessSession
    is valid for accessing PrivateEntity.
    2. Should succeed because the run-as identity of RunAsStatelessSession
    is valid for accessing RunAsStatelessSession.excluded().
    3. Should fail because the run-as identity of RunAsStatelessSession
    is not Echo.
    */
   public void testRunAs() throws Exception
   {
      log.debug("+++ testRunAs");
      login();
      StatelessSession bean = (StatelessSession)getInitialContext().lookup("spec.RunAsStatelessSession");
      log.debug("Created spec.RunAsStatelessSession");
      log.debug("Bean.echo('testRunAs') -> "+bean.echo("testRunAs"));
      bean.noop();
      log.debug("Bean.noop(), ok");
      
      try
      {
         // This should not be allowed
         bean.forward("Hello");
         fail("Was able to call RunAsStatelessSession.forward");
      }
      catch(Exception e)
      {
         log.debug("StatelessSession.forward failed as expected");
      }
      
      logout();
   }

   /** This method tests the following call chain:
    Level1CallerBean.callEcho() -> Level2CallerBean.invokeEcho()
      -> Level3CalleeBean.echo()
    The Level1CallerBean uses a run-as of InternalRole and the Level2CallerBean
    and Level3CalleeBean are only accessible by InternalRole.
    */   
   public void testDeepRunAs() throws Exception
   {
      log.debug("+++ testDeepRunAs");
      login();
     
      CalledSession bean = (CalledSession)getInitialContext().lookup("spec.Level1CallerBean");
      log.debug("Created spec.Level1CallerBean");
      bean.callEcho();
      log.debug("Bean.callEcho() ok");
   }

   public void testRunAsSFSB() throws Exception
   {
      log.info("+++ testRunAsSFSB");
      login();
      log.debug("Found CallerFacadeBean-testRunAsSFSB Home");
      CalledSession bean = (CalledSession)getInitialContext().lookup("spec.CallerFacadeBean-testRunAsSFSB");
      log.debug("Created spec.CallerFacadeBean-testRunAsSFSB");
      bean.invokeEcho("testRunAsSFSB");
      log.debug("Bean.invokeEcho() ok");
   }

   /**
    * Test the run-as side-effects raised in 
    * http://jira.jboss.com/jira/browse/JBAS-1852
    * 
    * @throws Exception
    */ 
   public void testJBAS1852() throws Exception
   {
      log.info("+++ testJBAS1852");
      login();
      SessionFacade bean = (SessionFacade)getInitialContext().lookup("spec.PublicSessionFacade");
      log.debug("Created PublicSessionFacade");
      log.debug("Bean.callEcho('testJBAS1852') -> " + bean.callEcho("testJBAS1852"));
   }

   /** Test that an MDB with a run-as identity is able to access secure EJBs
    that require the identity.
    */
   public void a1testMDBRunAs() throws Exception
   {
      log.debug("+++ testMDBRunAs");
      logout();
      QueueConnectionFactory queueFactory = (QueueConnectionFactory) getInitialContext().lookup(QUEUE_FACTORY);
      Queue queA = (Queue) getInitialContext().lookup("queue/A");
      Queue queB = (Queue) getInitialContext().lookup("queue/B");
      QueueConnection queueConn = queueFactory.createQueueConnection();
      QueueSession session = queueConn.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
      Message msg = session.createMessage();
      msg.setStringProperty("arg", "testMDBRunAs");
      msg.setJMSReplyTo(queB);
      QueueSender sender = session.createSender(queA);
      sender.send(msg);
      sender.close();
      log.debug("Sent msg to queue/A");
      queueConn.start();
      QueueReceiver recv = session.createReceiver(queB);
      msg = recv.receive(5000);
      log.debug("Recv msg: "+msg);
      String info = msg.getStringProperty("reply");
      recv.close();
      session.close();
      queueConn.close();

      if( info == null || info.startsWith("Failed") )
      {
         fail("Recevied exception reply, info="+info);
      }
   }

   /** Test that an MDB with a run-as identity is able to access secure EJBs
    that require the identity. DeepRunAsMDB -> Level1MDBCallerBean.callEcho() ->
      Level2CallerBean.invokeEcho() -> Level3CalleeBean.echo()
    The MDB uses a run-as of InternalRole and the Level2CallerBean
    and Level3CalleeBean are only accessible by InternalRole.
    */
   public void a1testMDBDeepRunAs() throws Exception
   {
      log.debug("+++ testMDBDeepRunAs");
      logout();
      QueueConnectionFactory queueFactory = (QueueConnectionFactory) getInitialContext().lookup(QUEUE_FACTORY);
      Queue queD = (Queue) getInitialContext().lookup("queue/D");
      Queue queB = (Queue) getInitialContext().lookup("queue/B");
      QueueConnection queueConn = queueFactory.createQueueConnection();
      QueueSession session = queueConn.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
      Message msg = session.createMessage();
      msg.setStringProperty("arg", "testMDBDeepRunAs");
      msg.setJMSReplyTo(queB);
      QueueSender sender = session.createSender(queD);
      sender.send(msg);
      sender.close();
      log.debug("Sent msg to "+queD);
      queueConn.start();
      QueueReceiver recv = session.createReceiver(queB);
      msg = recv.receive(5000);
      log.debug("Recv msg: "+msg);
      String info = msg.getStringProperty("reply");
      recv.close();
      session.close();
      queueConn.close();

      if( info == null || info.startsWith("Failed") )
      {
         fail("Recevied exception reply, info="+info);
      }
   }

   /** This method tests that the RunAsWithRolesMDB is assigned multiple roles
    * within its onMessage so that it can call into the ProjRepository session
    * bean's methods that required ProjectAdmin, CreateFolder and DeleteFolder
    * roles.
    */
   public void a1testRunAsWithRoles() throws Exception
   {
      log.debug("+++ testRunAsWithRoles");
      logout();
      QueueConnectionFactory queueFactory = (QueueConnectionFactory) getInitialContext().lookup(QUEUE_FACTORY);
      Queue queC = (Queue) getInitialContext().lookup("queue/C");
      Queue queB = (Queue) getInitialContext().lookup("queue/B");
      QueueConnection queueConn = queueFactory.createQueueConnection();
      QueueSession session = queueConn.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
      Message msg = session.createMessage();
      msg.setStringProperty("name", "testRunAsWithRoles");
      msg.setJMSReplyTo(queB);
      QueueSender sender = session.createSender(queC);
      sender.send(msg);
      sender.close();
      log.debug("Sent msg to queue/C");
      queueConn.start();
      QueueReceiver recv = session.createReceiver(queB);
      msg = recv.receive(5000);
      log.debug("Recv msg: "+msg);
      String info = msg.getStringProperty("reply");
      recv.close();
      session.close();
      queueConn.close();

      if( info == null || info.startsWith("Failed") )
      {
         fail("Recevied exception reply, info="+info);
      }
   }

   /** Login as user scott using the conf.name login config or
    'spec-test' if conf.name is not defined.
    */
   private void login() throws Exception
   {
      login(username, password);
   }
   private void login(String username, char[] password) throws Exception
   {
      if( loggedIn )
         return;
      
      String confName = System.getProperty("conf.name", "spec-test");
      AppCallbackHandler handler = new AppCallbackHandler(username, password);
      log.debug("Creating LoginContext("+confName+")");
      lc = new LoginContext(confName, handler);
      lc.login();
      log.debug("Created LoginContext, subject="+lc.getSubject());
      loggedIn = true;
   }
   private void logout() throws Exception
   {
      if( lc != null )
      {
         loggedIn = false;
         lc.logout();
         lc = null;
      }
   }


   /**
    * Setup the test suite.
    */
   public static Test suite() throws Exception
   {
      Configuration.setConfiguration(new XMLLoginConfigImpl());
      return getDeploySetup(EJBSpecUnitTestCase.class, "security.jar");
   }
}
