/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.activemq.security;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.naming.Context;
import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.directory.DirContext;

import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTopic;
import org.apache.activemq.jaas.GroupPrincipal;
import org.apache.activemq.util.Wait;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
import org.apache.directory.shared.ldap.model.ldif.LdifEntry;
import org.apache.directory.shared.ldap.model.ldif.LdifReader;
import org.apache.directory.shared.ldap.model.message.ModifyRequest;
import org.apache.directory.shared.ldap.model.message.ModifyRequestImpl;
import org.apache.directory.shared.ldap.model.name.Dn;
import org.apache.directory.shared.ldap.model.name.Rdn;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public abstract class AbstractCachedLDAPAuthorizationMapLegacyTest extends AbstractLdapTestUnit {

   static final GroupPrincipal GUESTS = new GroupPrincipal("guests");
   static final GroupPrincipal USERS = new GroupPrincipal("users");
   static final GroupPrincipal ADMINS = new GroupPrincipal("admins");

   protected LdapConnection connection;
   protected SimpleCachedLDAPAuthorizationMap map;

   @Before
   public void setup() throws Exception {
      connection = getLdapConnection();
      map = createMap();
   }

   @After
   public void cleanup() throws Exception {
      if (connection != null) {
         try {
            connection.close();
         }
         catch (IOException e) {
            // Ignore
         }
      }

      if (map != null) {
         map.destroy();
      }
   }

   @Test
   public void testQuery() throws Exception {
      map.query();
      Set<?> readACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + readACLs, 2, readACLs.size());
      assertTrue("Contains admin group", readACLs.contains(ADMINS));
      assertTrue("Contains users group", readACLs.contains(USERS));

      Set<?> failedACLs = map.getReadACLs(new ActiveMQQueue("FAILED"));
      assertEquals("set size: " + failedACLs, 0, failedACLs.size());
   }

   @Test
   public void testSynchronousUpdate() throws Exception {
      map.setRefreshInterval(1);
      map.query();
      Set<?> readACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + readACLs, 2, readACLs.size());
      assertTrue("Contains admin group", readACLs.contains(ADMINS));
      assertTrue("Contains users group", readACLs.contains(USERS));

      Set<?> failedACLs = map.getReadACLs(new ActiveMQQueue("FAILED"));
      assertEquals("set size: " + failedACLs, 0, failedACLs.size());

      LdifReader reader = new LdifReader(getRemoveLdif());

      for (LdifEntry entry : reader) {
         connection.delete(entry.getDn());
      }

      reader.close();

      assertTrue("did not get expected size. ", Wait.waitFor(new Wait.Condition() {

         @Override
         public boolean isSatisified() throws Exception {
            return map.getReadACLs(new ActiveMQQueue("TEST.FOO")).size() == 0;
         }
      }));

      assertNull(map.getTempDestinationReadACLs());
      assertNull(map.getTempDestinationWriteACLs());
      assertNull(map.getTempDestinationAdminACLs());
   }

   @Test
   public void testWildcards() throws Exception {
      map.query();
      Set<?> fooACLs = map.getReadACLs(new ActiveMQQueue("FOO.1"));
      assertEquals("set size: " + fooACLs, 2, fooACLs.size());
      assertTrue("Contains admin group", fooACLs.contains(ADMINS));
      assertTrue("Contains users group", fooACLs.contains(USERS));

      Set<?> barACLs = map.getReadACLs(new ActiveMQQueue("BAR.2"));
      assertEquals("set size: " + barACLs, 2, barACLs.size());
      assertTrue("Contains admin group", barACLs.contains(ADMINS));
      assertTrue("Contains users group", barACLs.contains(USERS));
   }

   @Test
   public void testAdvisory() throws Exception {
      map.query();
      Set<?> readACLs = map.getReadACLs(new ActiveMQTopic("ActiveMQ.Advisory.Connection"));
      assertEquals("set size: " + readACLs, 2, readACLs.size());
      assertTrue("Contains admin group", readACLs.contains(ADMINS));
      assertTrue("Contains users group", readACLs.contains(USERS));
   }

   @Test
   public void testTemporary() throws Exception {
      map.query();

      Thread.sleep(1000);
      Set<?> readACLs = map.getTempDestinationReadACLs();
      assertEquals("set size: " + readACLs, 2, readACLs.size());
      assertTrue("Contains admin group", readACLs.contains(ADMINS));
      assertTrue("Contains users group", readACLs.contains(USERS));
   }

   @Test
   public void testAdd() throws Exception {
      map.query();

      Set<?> failedACLs = map.getReadACLs(new ActiveMQQueue("FAILED"));
      assertEquals("set size: " + failedACLs, 0, failedACLs.size());

      LdifReader reader = new LdifReader(getAddLdif());

      for (LdifEntry entry : reader) {
         connection.add(entry.getEntry());
      }

      reader.close();

      Thread.sleep(2000);

      failedACLs = map.getReadACLs(new ActiveMQQueue("FAILED"));
      assertEquals("set size: " + failedACLs, 2, failedACLs.size());
   }

   @Test
   public void testRemove() throws Exception {
      map.query();

      Set<?> failedACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 2, failedACLs.size());

      LdifReader reader = new LdifReader(getRemoveLdif());

      for (LdifEntry entry : reader) {
         connection.delete(entry.getDn());
      }

      reader.close();
      Thread.sleep(2000);

      failedACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 0, failedACLs.size());

      assertTrue(map.getTempDestinationReadACLs() == null || map.getTempDestinationReadACLs().isEmpty());
      assertTrue(map.getTempDestinationWriteACLs() == null || map.getTempDestinationWriteACLs().isEmpty());
      assertTrue(map.getTempDestinationAdminACLs() == null || map.getTempDestinationAdminACLs().isEmpty());
   }

   @Test
   public void testRenameDestination() throws Exception {
      map.query();

      // Test for a destination rename
      Set<?> failedACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 2, failedACLs.size());

      connection.rename(new Dn("cn=TEST.FOO," + getQueueBaseDn()), new Rdn("cn=TEST.BAR"));

      Thread.sleep(2000);

      failedACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 0, failedACLs.size());

      failedACLs = map.getReadACLs(new ActiveMQQueue("TEST.BAR"));
      assertEquals("set size: " + failedACLs, 2, failedACLs.size());
   }

   @Test
   public void testRenamePermission() throws Exception {
      map.query();

      // Test for a permission rename
      connection.delete(new Dn("cn=Read,cn=TEST.FOO," + getQueueBaseDn()));

      Thread.sleep(2000);

      Set<?> failedACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 0, failedACLs.size());

      failedACLs = map.getWriteACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 2, failedACLs.size());

      connection.rename(new Dn("cn=Write,cn=TEST.FOO," + getQueueBaseDn()), new Rdn("cn=Read"));

      Thread.sleep(2000);

      failedACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 2, failedACLs.size());

      failedACLs = map.getWriteACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 0, failedACLs.size());
   }

   @Test
   public void testChange() throws Exception {
      map.query();

      // Change permission entry
      Set<?> failedACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 2, failedACLs.size());

      Dn dn = new Dn("cn=read,cn=TEST.FOO," + getQueueBaseDn());

      ModifyRequest request = new ModifyRequestImpl();
      request.setName(dn);
      setupModifyRequest(request);

      connection.modify(request);

      Thread.sleep(2000);

      failedACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 1, failedACLs.size());

      // Change destination entry
      request = new ModifyRequestImpl();
      request.setName(new Dn("cn=TEST.FOO," + getQueueBaseDn()));
      request.add("description", "This is a description!  In fact, it is a very good description.");

      connection.modify(request);

      Thread.sleep(2000);

      failedACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 1, failedACLs.size());
   }

   @Test
   public void testRestartAsync() throws Exception {
      testRestart(false);
   }

   @Test
   public void testRestartSync() throws Exception {
      testRestart(true);
   }

   public void testRestart(final boolean sync) throws Exception {
      if (sync) {
         // ldap connection can be slow to close
         map.setRefreshInterval(1000);
      }
      map.query();

      Set<?> failedACLs = map.getReadACLs(new ActiveMQQueue("FAILED"));
      assertEquals("set size: " + failedACLs, 0, failedACLs.size());

      failedACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 2, failedACLs.size());

      getLdapServer().stop();

      // wait for the context to be closed
      // as we can't rely on ldar server isStarted()
      Wait.waitFor(new Wait.Condition() {
         @Override
         public boolean isSatisified() throws Exception {
            if (sync) {
               return !map.isContextAlive();
            }
            else {
               return map.context == null;
            }
         }
      });

      failedACLs = map.getReadACLs(new ActiveMQQueue("TEST.FOO"));
      assertEquals("set size: " + failedACLs, 2, failedACLs.size());

      getLdapServer().start();

      Thread.sleep(2000);

      connection = getLdapConnection();

      LdifReader reader = new LdifReader(getAddLdif());

      for (LdifEntry entry : reader) {
         connection.add(entry.getEntry());
      }

      reader.close();

      assertTrue("did not get expected size. ", Wait.waitFor(new Wait.Condition() {

         @Override
         public boolean isSatisified() throws Exception {
            return map.getReadACLs(new ActiveMQQueue("FAILED")).size() == 2;
         }
      }));
   }

   protected SimpleCachedLDAPAuthorizationMap createMap() {
      return new SimpleCachedLDAPAuthorizationMap();
   }

   protected abstract InputStream getAddLdif();

   protected abstract InputStream getRemoveLdif();

   protected void setupModifyRequest(ModifyRequest request) {
      request.remove("member", "cn=users");
   }

   protected abstract String getQueueBaseDn();

   protected abstract LdapConnection getLdapConnection() throws Exception;

   public static void cleanAndLoad(String deleteFromDn,
                                   String ldifResourcePath,
                                   String ldapHost,
                                   int ldapPort,
                                   String ldapUser,
                                   String ldapPass,
                                   DirContext context) throws Exception {
      // Cleanup everything used for testing.
      List<String> dns = new LinkedList<>();
      dns.add(deleteFromDn);

      while (!dns.isEmpty()) {
         String name = dns.get(dns.size() - 1);
         Context currentContext = (Context) context.lookup(name);
         NamingEnumeration<NameClassPair> namingEnum = currentContext.list("");

         if (namingEnum.hasMore()) {
            while (namingEnum.hasMore()) {
               dns.add(namingEnum.next().getNameInNamespace());
            }
         }
         else {
            context.unbind(name);
            dns.remove(dns.size() - 1);
         }
      }

      // A bit of a hacked approach to loading an LDIF into OpenLDAP since there isn't an easy way to do it
      // otherwise.  This approach invokes the command line tool programmatically but has
      // to short-circuit the call to System.exit that the command line tool makes when it finishes.
      // We are assuming that there isn't already a security manager in place.
      final SecurityManager securityManager = new SecurityManager() {

         @Override
         public void checkPermission(java.security.Permission permission) {
            if (permission.getName().contains("exitVM")) {
               throw new SecurityException("System.exit calls disabled for the moment.");
            }
         }
      };

      System.setSecurityManager(securityManager);

      File file = new File(AbstractCachedLDAPAuthorizationMapLegacyTest.class.getClassLoader().getResource(ldifResourcePath).toURI());

      Class<?> clazz = Class.forName("LDAPModify");
      Method mainMethod = clazz.getMethod("main", String[].class);

      try {
         mainMethod.invoke(null, new Object[]{new String[]{"-v", "-h", ldapHost, "-p", String.valueOf(ldapPort), "-D", ldapUser, "-w", ldapPass, "-a", "-f", file.toString()}});
      }
      catch (InvocationTargetException e) {
         if (!(e.getTargetException() instanceof SecurityException)) {
            throw e;
         }
      }

      System.setSecurityManager(null);
   }
}


