/*
 * Copyright (C) 2005-2010 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco 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 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco 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 Alfresco. If not, see .
 */
package org.alfresco.repo.security.authentication;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.transaction.Status;
import javax.transaction.UserTransaction;
import junit.framework.TestCase;
import net.sf.acegisecurity.AccountExpiredException;
import net.sf.acegisecurity.Authentication;
import net.sf.acegisecurity.AuthenticationManager;
import net.sf.acegisecurity.BadCredentialsException;
import net.sf.acegisecurity.CredentialsExpiredException;
import net.sf.acegisecurity.DisabledException;
import net.sf.acegisecurity.LockedException;
import net.sf.acegisecurity.UserDetails;
import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.management.subsystems.ChildApplicationContextManager;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.authentication.InMemoryTicketComponentImpl.ExpiryMode;
import org.alfresco.repo.security.authentication.InMemoryTicketComponentImpl.Ticket;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.MutableAuthenticationService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.DynamicNamespacePrefixResolver;
import org.alfresco.service.namespace.NamespacePrefixResolver;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.PostgreSQLDialect;
import org.springframework.context.ApplicationContext;
@SuppressWarnings("unchecked")
public class AuthenticationTest extends TestCase
{
    private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
    private NodeService nodeService;
    private AuthorityService authorityService;
    private TenantService tenantService;
    private MD4PasswordEncoder passwordEncoder;
    private MutableAuthenticationDao dao;
    private AuthenticationManager authenticationManager;
    private TicketComponent ticketComponent;
    private SimpleCache ticketsCache;
    private MutableAuthenticationService authenticationService;
    private MutableAuthenticationService pubAuthenticationService;
    private AuthenticationComponent authenticationComponent;
    private AuthenticationComponent authenticationComponentImpl;
    private TransactionService transactionService;
    private PersonService pubPersonService;
    private PersonService personService;
    private UserTransaction userTransaction;
    private NodeRef rootNodeRef;
    private NodeRef systemNodeRef;
    private NodeRef typesNodeRef;
    private NodeRef personAndyNodeRef;
    // TODO: pending replacement
    private Dialect dialect;
    private PolicyComponent policyComponent;
    private SimpleCache authenticationCache;    
    public AuthenticationTest()
    {
        super();
    }
    public AuthenticationTest(String arg0)
    {
        super(arg0);
    }
    public void setUp() throws Exception
    {
        if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_NONE)
        {
            throw new AlfrescoRuntimeException(
                    "A previous tests did not clean up transaction: " +
                    AlfrescoTransactionSupport.getTransactionId());
        }
        
        dialect = (Dialect) ctx.getBean("dialect");
        
        nodeService = (NodeService) ctx.getBean("nodeService");
        authorityService = (AuthorityService) ctx.getBean("authorityService");
        tenantService = (TenantService) ctx.getBean("tenantService");
        passwordEncoder = (MD4PasswordEncoder) ctx.getBean("passwordEncoder");
        ticketComponent = (TicketComponent) ctx.getBean("ticketComponent");
        authenticationService = (MutableAuthenticationService) ctx.getBean("authenticationService");
        pubAuthenticationService = (MutableAuthenticationService) ctx.getBean("AuthenticationService");
        authenticationComponent = (AuthenticationComponent) ctx.getBean("authenticationComponent");
        authenticationComponentImpl = (AuthenticationComponent) ctx.getBean("authenticationComponent");
        pubPersonService =  (PersonService) ctx.getBean("PersonService");
        personService =  (PersonService) ctx.getBean("personService");
        policyComponent = (PolicyComponent) ctx.getBean("policyComponent");
        authenticationCache = (SimpleCache) ctx.getBean("authenticationCache");
        // permissionServiceSPI = (PermissionServiceSPI)
        // ctx.getBean("permissionService");
        ticketsCache = (SimpleCache) ctx.getBean("ticketsCache");
        dao = (MutableAuthenticationDao) ctx.getBean("authenticationDao");
        
        // Let's look inside the alfresco authentication subsystem to get the DAO-wired authentication manager
        ChildApplicationContextManager authenticationChain = (ChildApplicationContextManager) ctx.getBean("Authentication");
        ApplicationContext subsystem = authenticationChain.getApplicationContext(authenticationChain.getInstanceIds().iterator().next());
        authenticationManager = (AuthenticationManager) subsystem.getBean("authenticationManager");
        transactionService = (TransactionService) ctx.getBean(ServiceRegistry.TRANSACTION_SERVICE.getLocalName());
        userTransaction = transactionService.getUserTransaction();
        userTransaction.begin();
        StoreRef storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis());
        rootNodeRef = nodeService.getRootNode(storeRef);
        QName children = ContentModel.ASSOC_CHILDREN;
        QName system = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "system");
        QName container = ContentModel.TYPE_CONTAINER;
        QName types = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "people");
        systemNodeRef = nodeService.createNode(rootNodeRef, children, system, container).getChildRef();
        typesNodeRef = nodeService.createNode(systemNodeRef, children, types, container).getChildRef();
        Map props = createPersonProperties("Andy");
        personAndyNodeRef = nodeService.createNode(typesNodeRef, children, ContentModel.TYPE_PERSON, container, props).getChildRef();
        assertNotNull(personAndyNodeRef);
        
        AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName());
        
        deleteAndy();
        authenticationComponent.clearCurrentSecurityContext();
    }
    private void deleteAndy()
    {
        RepositoryAuthenticationDao dao = new RepositoryAuthenticationDao();
        dao.setAuthorityService(authorityService);
        dao.setTenantService(tenantService);
        dao.setNodeService(nodeService);
        dao.setNamespaceService(getNamespacePrefixReolsver(""));
        dao.setPasswordEncoder(passwordEncoder);
        dao.setPolicyComponent(policyComponent);
        dao.setAuthenticationCache(authenticationCache);
        if (dao.getUserOrNull("andy") != null)
        {
            dao.deleteUser("andy");
        }
        
        if(personService.personExists("andy"))
        {
            personService.deletePerson("andy");
        }
        
    }
    @Override
    protected void tearDown() throws Exception
    {
        if ((userTransaction.getStatus() == Status.STATUS_ACTIVE) || (userTransaction.getStatus() == Status.STATUS_MARKED_ROLLBACK))
        {
            userTransaction.rollback();
        }
        AuthenticationUtil.clearCurrentSecurityContext();
        super.tearDown();
    }
    private Map createPersonProperties(String userName)
    {
        HashMap properties = new HashMap();
        properties.put(ContentModel.PROP_USERNAME, "Andy");
        return properties;
    }
    public void testSystemTicket() throws Exception
    {
        assertNull(AuthenticationUtil.getFullAuthentication());
        assertNull(AuthenticationUtil.getRunAsAuthentication());
      
        authenticationComponent.setSystemUserAsCurrentUser();
        pubAuthenticationService.createAuthentication("andy", "andy".toCharArray());
        
        pubAuthenticationService.clearCurrentSecurityContext();
        
        assertNull(AuthenticationUtil.getFullAuthentication());
        assertNull(AuthenticationUtil.getRunAsAuthentication());
        
        // Authenticate
        pubAuthenticationService.authenticate("andy", "andy".toCharArray());
        
        // Get current user name
        String userName = pubAuthenticationService.getCurrentUserName();
        assertEquals("andy", userName);
        
        // Get ticket
        String ticket = pubAuthenticationService.getCurrentTicket();
        assertEquals("andy", ticketComponent.getAuthorityForTicket(ticket));
        
        // Get logged in user ...
        // Get userName
        userName = pubAuthenticationService.getCurrentUserName();
        assertEquals("andy", userName);
        // get Person
        assertTrue(pubPersonService.personExists(userName));
        
        AuthenticationUtil.runAs(new RunAsWork() {
            public Void doWork() throws Exception
            {
                // TODO Auto-generated method stub
                assertEquals("andy", ticketComponent.getAuthorityForTicket(pubAuthenticationService.getCurrentTicket()));
                return null;
            }}, AuthenticationUtil.getSystemUserName());
        
        pubPersonService.getPerson(userName);
        assertTrue(pubPersonService.personExists(userName));
        // re-getTicket
        String newticket = pubAuthenticationService.getCurrentTicket();
        assertEquals(ticket, newticket);
        assertEquals("andy", ticketComponent.getAuthorityForTicket(newticket));
        
        
        userName = pubAuthenticationService.getCurrentUserName();
        assertEquals("andy", userName);
        
        // new TX
        
        //userTransaction.commit();
        //userTransaction = transactionService.getUserTransaction();
        //userTransaction.begin();
        
        pubAuthenticationService.validate(ticket);
        userName = pubAuthenticationService.getCurrentUserName();
        assertEquals("andy", userName);
        
        pubAuthenticationService.validate(newticket);
        userName = pubAuthenticationService.getCurrentUserName();
        assertEquals("andy", userName);
        
    }
    
    public void xtestScalability()
    {
        long create = 0;
        long start;
        long end;
        authenticationComponent.authenticate(AuthenticationUtil.getAdminUserName(), "admin".toCharArray());
        for (int i = 0; i < 10000; i++)
        {
            String id = "TestUser-" + i;
            start = System.nanoTime();
            authenticationService.createAuthentication(id, id.toCharArray());
            end = System.nanoTime();
            create += (end - start);
            if ((i > 0) && (i % 100 == 0))
            {
                System.out.println("Count = " + i);
                System.out.println("Average create : " + (create / i / 1000000.0f));
                start = System.nanoTime();
                dao.userExists(id);
                end = System.nanoTime();
                System.out.println("Exists : " + ((end - start) / 1000000.0f));
            }
        }
        authenticationComponent.clearCurrentSecurityContext();
    }
    public void c()
    {
        try
        {
            authenticationService.authenticate("", "".toCharArray());
        }
        catch (AuthenticationException e)
        {
            // Expected
        }
    }
    public void testNewTicketOnLogin()
    {
        authenticationComponent.setSystemUserAsCurrentUser();
        pubAuthenticationService.createAuthentication("Andy", "auth1".toCharArray());
        authenticationComponent.clearCurrentSecurityContext();
        // authenticate with this user details
        pubAuthenticationService.authenticate("Andy", "auth1".toCharArray());
        String ticket1 = pubAuthenticationService.getCurrentTicket();
        pubAuthenticationService.authenticate("Andy", "auth1".toCharArray());
        assertFalse(ticket1.equals(pubAuthenticationService.getCurrentTicket()));
        
    }
    
    public void testGuest()
    {
        authenticationService.authenticate(AuthenticationUtil.getGuestUserName(), "".toCharArray());
    }
    public void testCreateUsers()
    {
        authenticationService.createAuthentication(AuthenticationUtil.getGuestUserName(), "".toCharArray());
        authenticationService.authenticate(AuthenticationUtil.getGuestUserName(), "".toCharArray());
        // Guest is treated like any other user
        assertEquals(AuthenticationUtil.getGuestUserName(), authenticationService.getCurrentUserName());
        authenticationService.createAuthentication("Andy", "".toCharArray());
        authenticationService.authenticate("Andy", "".toCharArray());
        assertEquals("Andy", authenticationService.getCurrentUserName());
        if (! tenantService.isEnabled())
        {
            authenticationService.createAuthentication("Mr.Woof.Banana@chocolate.chip.cookie.com", "".toCharArray());
            authenticationService.authenticate("Mr.Woof.Banana@chocolate.chip.cookie.com", "".toCharArray());
            assertEquals("Mr.Woof.Banana@chocolate.chip.cookie.com", authenticationService.getCurrentUserName());
        }
        else
        {
            // TODO - could create tenant domain 'chocolate.chip.cookie.com'
        }
        authenticationService.createAuthentication("Andy_Woof/Domain", "".toCharArray());
        authenticationService.authenticate("Andy_Woof/Domain", "".toCharArray());
        assertEquals("Andy_Woof/Domain", authenticationService.getCurrentUserName());
        authenticationService.createAuthentication("Andy_ Woof/Domain", "".toCharArray());
        authenticationService.authenticate("Andy_ Woof/Domain", "".toCharArray());
        assertEquals("Andy_ Woof/Domain", authenticationService.getCurrentUserName());
        if (! tenantService.isEnabled())
        {
            String un = "Andy `\u00ac\u00a6!\u00a3$%^&*()-_=+\t\n\u0000[]{};'#:@~,./<>?|";
            if (dialect instanceof PostgreSQLDialect)
            {
                // Note: PostgreSQL does not support \u0000 char embedded in a string
                // http://archives.postgresql.org/pgsql-jdbc/2007-02/msg00115.php
                un = "Andy `\u00ac\u00a6!\u00a3$%^&*()-_=+\t\n[]{};'#:@~,./<>?|";
            }
            
            authenticationService.createAuthentication(un, "".toCharArray());
            authenticationService.authenticate(un, "".toCharArray());
            assertEquals(un, authenticationService.getCurrentUserName());
        }
        else
        {
            // tenant domain ~,./<>?\\| is not valid format"
        }
    }
    public void testCreateAndyUserAndOtherCRUD() throws NoSuchAlgorithmException, UnsupportedEncodingException
    {
        RepositoryAuthenticationDao dao = new RepositoryAuthenticationDao();
        dao.setTenantService(tenantService);
        dao.setNodeService(nodeService);
        dao.setAuthorityService(authorityService);
        dao.setNamespaceService(getNamespacePrefixReolsver(""));
        dao.setPasswordEncoder(passwordEncoder);
        dao.setPolicyComponent(policyComponent);
        dao.setAuthenticationCache(authenticationCache);
        dao.createUser("Andy", "cabbage".toCharArray());
        assertNotNull(dao.getUserOrNull("Andy"));
        byte[] decodedHash = passwordEncoder.decodeHash(dao.getMD4HashedPassword("Andy"));
        byte[] testHash = MessageDigest.getInstance("MD4").digest("cabbage".getBytes("UnicodeLittleUnmarked"));
        assertEquals(new String(decodedHash), new String(testHash));
        UserDetails AndyDetails = (UserDetails) dao.loadUserByUsername("Andy");
        assertNotNull(AndyDetails);
        assertEquals("Andy", AndyDetails.getUsername());
        // assertNotNull(dao.getSalt(AndyDetails));
        assertTrue(AndyDetails.isAccountNonExpired());
        assertTrue(AndyDetails.isAccountNonLocked());
        assertTrue(AndyDetails.isCredentialsNonExpired());
        assertTrue(AndyDetails.isEnabled());
        assertNotSame("cabbage", AndyDetails.getPassword());
        assertEquals(AndyDetails.getPassword(), passwordEncoder.encodePassword("cabbage", dao.getSalt(AndyDetails)));
        assertEquals(1, AndyDetails.getAuthorities().length);
        // Object oldSalt = dao.getSalt(AndyDetails);
        dao.updateUser("Andy", "carrot".toCharArray());
        UserDetails newDetails = (UserDetails) dao.loadUserByUsername("Andy");
        assertNotNull(newDetails);
        assertEquals("Andy", newDetails.getUsername());
        // assertNotNull(dao.getSalt(newDetails));
        assertTrue(newDetails.isAccountNonExpired());
        assertTrue(newDetails.isAccountNonLocked());
        assertTrue(newDetails.isCredentialsNonExpired());
        assertTrue(newDetails.isEnabled());
        assertNotSame("carrot", newDetails.getPassword());
        assertEquals(1, newDetails.getAuthorities().length);
        assertNotSame(AndyDetails.getPassword(), newDetails.getPassword());
        // assertNotSame(oldSalt, dao.getSalt(newDetails));
        dao.deleteUser("Andy");
        assertNull(dao.getUserOrNull("Andy"));
        MessageDigest digester;
        try
        {
            digester = MessageDigest.getInstance("MD4");
            System.out.println("Digester from " + digester.getProvider());
        }
        catch (NoSuchAlgorithmException e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
            System.out.println("No digester");
        }
    }
    public void testAuthentication()
    {
        dao.createUser("GUEST", "".toCharArray());
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("GUEST", "");
        token.setAuthenticated(false);
        Authentication result = authenticationManager.authenticate(token);
        assertNotNull(result);
        dao.createUser("Andy", "squash".toCharArray());
        token = new UsernamePasswordAuthenticationToken("Andy", "squash");
        token.setAuthenticated(false);
        result = authenticationManager.authenticate(token);
        assertNotNull(result);
        dao.setEnabled("Andy", false);
        try
        {
            result = authenticationManager.authenticate(token);
            assertNotNull(result);
            assertNotNull(null);
        }
        catch (DisabledException e)
        {
            // Expected
        }
        dao.setEnabled("Andy", true);
        result = authenticationManager.authenticate(token);
        assertNotNull(result);
        dao.setLocked("Andy", true);
        try
        {
            result = authenticationManager.authenticate(token);
            assertNotNull(result);
            assertNotNull(null);
        }
        catch (LockedException e)
        {
            // Expected
        }
        dao.setLocked("Andy", false);
        result = authenticationManager.authenticate(token);
        assertNotNull(result);
        dao.setAccountExpires("Andy", true);
        dao.setCredentialsExpire("Andy", true);
        result = authenticationManager.authenticate(token);
        assertNotNull(result);
        dao.setAccountExpiryDate("Andy", null);
        dao.setCredentialsExpiryDate("Andy", null);
        result = authenticationManager.authenticate(token);
        assertNotNull(result);
        dao.setAccountExpiryDate("Andy", new Date(new Date().getTime() + 10000));
        dao.setCredentialsExpiryDate("Andy", new Date(new Date().getTime() + 10000));
        result = authenticationManager.authenticate(token);
        assertNotNull(result);
        dao.setAccountExpiryDate("Andy", new Date(new Date().getTime() - 10000));
        try
        {
            result = authenticationManager.authenticate(token);
            assertNotNull(result);
            assertNotNull(null);
        }
        catch (AccountExpiredException e)
        {
            // Expected
        }
        dao.setAccountExpiryDate("Andy", new Date(new Date().getTime() + 10000));
        result = authenticationManager.authenticate(token);
        assertNotNull(result);
        dao.setCredentialsExpiryDate("Andy", new Date(new Date().getTime() - 10000));
        try
        {
            result = authenticationManager.authenticate(token);
            assertNotNull(result);
            assertNotNull(null);
        }
        catch (CredentialsExpiredException e)
        {
            // Expected
        }
        dao.setCredentialsExpiryDate("Andy", new Date(new Date().getTime() + 10000));
        result = authenticationManager.authenticate(token);
        assertNotNull(result);
        dao.deleteUser("Andy");
        // assertNull(dao.getUserOrNull("Andy"));
    }
    public void testCreateAuthenticationWhileRunningAsSystem() throws Exception
    {
        RunAsWork