/*
 * JORAM: Java(TM) Open Reliable Asynchronous Messaging
 * Copyright (C) 2008 - 2009 ScalAgent Distributed Technologies
 *
 * This library 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 any later version.
 * 
 * This library 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 library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA.
 *
 * Initial developer(s): ScalAgent Distributed Technologies
 * Contributor(s): Abdenbi Benammour
 */
package org.objectweb.joram.shared.security.jaas;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.acl.Group;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Set;

import javax.naming.Context;
import javax.security.auth.Subject;
import javax.security.auth.login.AccountExpiredException;
import javax.security.auth.login.CredentialExpiredException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import org.objectweb.jonas.security.auth.JPrincipal;
import org.objectweb.jonas.security.auth.JSigned;
import org.objectweb.jonas.security.auth.callback.NoInputCallbackHandler;
import org.objectweb.joram.shared.security.Identity;
import org.objectweb.util.monolog.api.BasicLevel;

import fr.dyade.aaa.common.Configuration;
import fr.dyade.aaa.common.stream.StreamUtil;

/**
 * JAAS identity class used to authenticate through JOnAS.
 */
public class JonasIdentity extends Identity {
  /** Define serialVersionUID for interoperability. */
  private static final long serialVersionUID = 1L;

  /**
   * Name used in the JOnAS JAAS config file.
   */
  private static final String JAAS_ENTRY_NAME = "ask_remote";
  private static final String KEYSTORE_FILE = "joram.security.jaas.keystoreFile";
  private static final String KEYSTORE_PASS = "joram.security.jaas.keystorePass";
  private static final String KEYSTORE_ALIAS = "joram.security.jaas.alias";
  private static final String UNTESTED_SIGNATURE = "joram.security.jaas.untestedSignature";
  private static final String UNSORT_ROLES = "joram.security.jaas.unsortRoles";

  private String principal;
  private Subject subject = null;
  private LoginContext loginContext;
  private PublicKey publickey = null;

  /* (non-Javadoc)
   * @see org.objectweb.joram.shared.security.Identity#setIdentity(java.lang.String, java.lang.String)
   */
  public void setIdentity(String user, String passwd) throws Exception {
	 setIdentity(user, passwd, null); 
  }
  /**
   * sets the identity based on the user, password and the JAAS entry name.
   * @param user the username
   * @param passwd the password
   * @param pJaasEntryName the JAAS entry name (within the JAAS configuration file).
   * @throws Exception if an error occurs while trying to authenticate.
   */
  public void setIdentity(String user, String passwd, String pJaasEntryName) throws Exception {
    if (logger.isLoggable(BasicLevel.DEBUG))
      logger.log(BasicLevel.DEBUG, "JonasIdentity.setIdentity(" + user + ", ****)");

    principal = user;
    if (logger.isLoggable(BasicLevel.DEBUG))
      logger.log(BasicLevel.DEBUG, 
          "JonasIdentity.setIdentity principal = " + principal);

    try {
      String jaasEntryName=null;
      if (Configuration.getBoolean("joram.security.jaas.entryName")) {
    	  jaasEntryName = Configuration.getProperty("joram.security.jaas.entryName");
      } else if (pJaasEntryName!=null) {
    	  jaasEntryName = pJaasEntryName;
      } else {
    	  jaasEntryName = JAAS_ENTRY_NAME; //default
      }
      if (logger.isLoggable(BasicLevel.DEBUG))
        logger.log(BasicLevel.DEBUG, "setIdentity: jaasEntryName = "  + jaasEntryName);
      loginContext = new LoginContext(jaasEntryName, new NoInputCallbackHandler(user, passwd));
    } catch (LoginException e) {
      if (logger.isLoggable(BasicLevel.ERROR))
        logger.log(BasicLevel.ERROR, "EXCEPTION setIdentity::",e);
      throw new Exception(e.getMessage());
    }

    // Negotiate a login via this LoginContext
    try {
      if (logger.isLoggable(BasicLevel.DEBUG))
        logger.log(BasicLevel.DEBUG, 
            "JonasIdentity.setIdentity factory initial = " + 
            Configuration.getProperty(Context.INITIAL_CONTEXT_FACTORY)); 
      loginContext.login();     
      subject = loginContext.getSubject();
      if (subject == null) {
        if (logger.isLoggable(BasicLevel.ERROR))
          logger.log(BasicLevel.ERROR, "No subject for the user " + principal);
        throw new Exception("No subject for the user " + principal);
      }

    } catch (AccountExpiredException e) {
      if (logger.isLoggable(BasicLevel.ERROR))
        logger.log(BasicLevel.ERROR, "Account expired for the user " + principal, e);
      throw new Exception("Account expired for the user " + principal, e);
    } catch (CredentialExpiredException e) {
      if (logger.isLoggable(BasicLevel.ERROR))
        logger.log(BasicLevel.ERROR, "Credential expired for the user " + principal, e);
      throw new Exception("Credential expired for the user " + principal, e);
    } catch (FailedLoginException e) {
      if (logger.isLoggable(BasicLevel.ERROR))
        logger.log(BasicLevel.ERROR, "Failed Login for the user " + principal, e);
      throw new Exception("Failed Login for the user " + principal, e);
    } catch (LoginException e) {
      if (logger.isLoggable(BasicLevel.ERROR))
        logger.log(BasicLevel.ERROR, "Login exception for the user " + principal, e);
      throw new Exception("Login exception for the user " + principal, e);
    }
  }

//  /**
//   * Gets the subject of identity.
//   * 
//   * @return the subject of identity.
//   */
//  public Subject getSubject() {
//    return subject; 
//  }

//  /* (non-Javadoc)
//   * @see org.objectweb.joram.shared.security.Identity#getCredential()
//   */
//  public Object getCredential() {
//    return serializeSubject(subject);
//  }

  /* (non-Javadoc)
   * @see org.objectweb.joram.shared.security.Identity#getUserName()
   */
  public String getUserName() {
    return principal;
  }

  /* (non-Javadoc)
   * @see org.objectweb.joram.shared.security.Identity#setUserName(java.lang.String)
   */
  public void setUserName(String userName) {
    this.principal = userName;
  }

  private String getPrincipal() {
    // Retrieve the principal (members of the Group.class)    
    Set principals = subject.getPrincipals(Principal.class);
    Iterator iterator = principals.iterator();
    while (iterator.hasNext()) {
      Principal principal = (Principal) iterator.next();
      if (logger.isLoggable(BasicLevel.DEBUG))
        logger.log(BasicLevel.DEBUG, "getPrincipal class = " + principal.getClass().getName());
      // Signed group (empty group that contains a signature)?
      if (principal instanceof JPrincipal) {
        if (logger.isLoggable(BasicLevel.DEBUG))
          logger.log(BasicLevel.DEBUG, "getPrincipal name = " + ((JPrincipal)principal).getName());
        return principal.getName();
      }
    }
    return null;
  }

  /**
   * @return
   */
  private Object[] getRoles() {
    ArrayList principalRoles = new ArrayList();
    // Retrieve all roles of the user (Roles are members of the Group.class)
    Set principals = subject.getPrincipals(Group.class);
    Iterator iterator = principals.iterator();
    while (iterator.hasNext()) {
      Group group = (Group) iterator.next();
      // Signed group (empty group that contains a signature)?
      if (group instanceof JSigned) {
        continue;
      }
      Enumeration e = group.members();
      while (e.hasMoreElements()) {
        Principal p = (Principal) e.nextElement();
        principalRoles.add(p.getName());
      }
    }
    return principalRoles.toArray();
  }


  /**
   * @return
   */
  private  byte[] getSignature() { 
    // Retrieve signature (members of the Group.class)
    Set principals = subject.getPrincipals(Group.class);
    Iterator iterator = principals.iterator();
    while (iterator.hasNext()) {
      Group group = (Group) iterator.next();
      if (logger.isLoggable(BasicLevel.DEBUG))
        logger.log(BasicLevel.DEBUG, "getSignature group = " + group.getClass().getName());
      // Signed group (empty group that contains a signature)?
      if (group instanceof JSigned) {
        return ((JSigned) group).getSignature();
      }
    }
    return null;
  }

  /**
   * @throws Exception
   */
  private synchronized void initPublicKey() throws Exception {
    // Keystore file
    String keystoreFile = Configuration.getProperty(KEYSTORE_FILE);
    if (keystoreFile == null) {
      throw new IllegalStateException("The '" + KEYSTORE_FILE + "' attribute was not found but this attribute is mandatory");
    }

    // Keystore pass
    String keystorePass = Configuration.getProperty(KEYSTORE_PASS);
    if (keystorePass == null) {
      throw new IllegalStateException("The '" + KEYSTORE_PASS + "' attribute was not found but this attribute is mandatory");
    }

    // Alias
    String alias = Configuration.getProperty(KEYSTORE_ALIAS);
    if (alias == null) {
      throw new IllegalStateException("The '" + KEYSTORE_ALIAS + "' attribute was not found but this attribute is mandatory");
    }

    // Check that the file exists
    File f = new File(keystoreFile);
    if (!f.exists()) {
      throw new IllegalStateException("The keystore file named '" + f + "' was not found.");
    }

    // Gets the keystore instance
    KeyStore keyStore = null;
    try {
      keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    } catch (KeyStoreException e) {
      throw new IllegalStateException("Error while getting a keystore ':" + e.getMessage());
    }

    // Load the keystore file
    try {
      keyStore.load(new BufferedInputStream(new FileInputStream(f)), keystorePass.toCharArray());
    } catch (NoSuchAlgorithmException e) {
      throw new IllegalStateException("Error while loading the keystore file '" + f + "'." + e.getMessage());
    } catch (CertificateException e) {
      throw new IllegalStateException("Error while loading the keystore file '" + f + "'." + e.getMessage());
    } catch (FileNotFoundException e) {
      throw new IllegalStateException("Error while loading the keystore file '" + f + "'." + e.getMessage());
    } catch (IOException e) {
      throw new IllegalStateException("Error while loading the keystore file '" + f + "'." + e.getMessage());
    }

    // Get certificate
    Certificate cert;
    try {
      cert = keyStore.getCertificate(alias);
    } catch (KeyStoreException e) {
      throw new IllegalStateException("Error while getting the alias '" + alias + "' in the keystore file '" + keystoreFile + "':" + e.getMessage());
    }

    // set the public key
    publickey = cert.getPublicKey();
  }

  /**
   * @return
   * @throws Exception
   */
  private PublicKey getPublicKey() throws Exception {
    if (publickey == null)
      initPublicKey();
    return publickey;
  }

  private boolean validate(JonasIdentity identity) throws Exception {
    if (logger.isLoggable(BasicLevel.DEBUG))
      logger.log(BasicLevel.DEBUG, "validate(" + identity + ')');

    if (Configuration.getBoolean(UNTESTED_SIGNATURE)) {
      if (logger.isLoggable(BasicLevel.WARN)) {
        logger.log(BasicLevel.WARN, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
        logger.log(BasicLevel.WARN, "!!!!!!!!!!!! untested signature.");
        logger.log(BasicLevel.WARN, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
      }
      return true;
    }

    // Get public key
    PublicKey publickey = getPublicKey();

    return validate(publickey, identity);
  }

  private boolean validate(PublicKey publickey, JonasIdentity identity) throws Exception {

    // Build signature with data to validate (principal name + roles)
    Signature signature = null;
    try {
      signature = Signature.getInstance("SHA1withDSA");
    } catch (NoSuchAlgorithmException e) {
      if (logger.isLoggable(BasicLevel.ERROR))
        logger.log(BasicLevel.ERROR, "EXCEPTION:: validate", e);
      throw new Exception("Error while getting the algorithm 'SHA1withDSA' :" + e.getMessage());
    }

    try {
      signature.initVerify(publickey);
    } catch (InvalidKeyException e) {
      if (logger.isLoggable(BasicLevel.ERROR))
        logger.log(BasicLevel.ERROR, "EXCEPTION:: validate", e);
      throw new Exception("Cannot initialize the signature with the given public key:" + e.getMessage());
    }

    // Add principal name
    String principal = null;
    try {
      //signature.update(principal.getBytes());
      principal = identity.getPrincipal();
      if (logger.isLoggable(BasicLevel.DEBUG))
        logger.log(BasicLevel.DEBUG, "validate principal = " + principal);

      if (principal == null) {
        if (logger.isLoggable(BasicLevel.ERROR))
          logger.log(BasicLevel.ERROR, "EXCEPTION:: validate principal == null");
        throw new Exception("Cannot add the bytes for the principal name '" + principal + "'");      
      }

      signature.update(principal.getBytes());

    } catch (SignatureException e) {
      if (logger.isLoggable(BasicLevel.ERROR))
        logger.log(BasicLevel.ERROR, "EXCEPTION:: validate", e);
      throw new Exception("Cannot add the bytes for the principal name '" + principal + "' :" + e.getMessage());
    }

    // Add roles
    Object[] roles = identity.getRoles();
    if (!Configuration.getBoolean(UNSORT_ROLES)) {
      // Sort roles before adding it to the signature to preserve the order
      Arrays.sort(roles);
    }
    for (int r = 0; r < roles.length; r++) {
      try {
        signature.update(((String)roles[r]).getBytes());
      } catch (SignatureException e) {
        if (logger.isLoggable(BasicLevel.ERROR))
          logger.log(BasicLevel.ERROR, "EXCEPTION:: validate", e);
        throw new Exception("Cannot add the bytes for the role '" + roles[r] + "' : " + e.getMessage());
      }
    }

    // Check signature
    boolean trusted = false;
    try {
      trusted = signature.verify(identity.getSignature());
    } catch (SignatureException e) {
      if (logger.isLoggable(BasicLevel.ERROR))
        logger.log(BasicLevel.ERROR, "EXCEPTION:: validate", e);
      throw new Exception("The signature found in the identity '" + this + "' is invalid:" + e.getMessage());
    }

    // Invalid signature !
    if (!trusted) {
      if (logger.isLoggable(BasicLevel.ERROR))
        logger.log(BasicLevel.ERROR, "validate trusted = false");
      throw new Exception("The signature for the identity '" + this + "' has been altered by an unknown source.");
    }
    if (logger.isLoggable(BasicLevel.DEBUG))
      logger.log(BasicLevel.DEBUG, "validate trusted = " + trusted);

    return trusted;
  }

  /* (non-Javadoc)
   * @see org.objectweb.joram.shared.security.Identity#check(org.objectweb.joram.shared.security.Identity)
   */
  public boolean check(Identity identity) throws Exception {
    if (logger.isLoggable(BasicLevel.DEBUG))
      logger.log(BasicLevel.DEBUG, "JonasIdentity.check(" + identity + ')');

    if (! (identity instanceof JonasIdentity)) {
      if (logger.isLoggable(BasicLevel.ERROR))
        logger.log(BasicLevel.ERROR, "check : JonasIdentity is not an instance of " + identity);
      throw new Exception("check : JonasIdentity is not an instance of " + identity);
    }

    return validate((JonasIdentity) identity);
  }

//  /**
//   * remove unserialized object.
//   * @param subject
//   * @return
//   */
//  private Subject serializeSubject(final Subject subject) {
//    Subject subjectSer = null;
//    try {
//      PipedOutputStream pos = new PipedOutputStream();
//      PipedInputStream pis = new PipedInputStream(pos);
//      final ObjectOutputStream oos = new ObjectOutputStream(pos);
//      new Thread() {
//        public void run() {
//          try {
//            oos.writeObject(subject);
//            oos.flush();
//          } catch (IOException e) {
//            if (logger.isLoggable(BasicLevel.ERROR))
//              logger.log(BasicLevel.ERROR, "EXCEPTION:: serializeSubject Thread", e);
//          }
//        }
//      }.start();
//      ObjectInputStream ois = new ObjectInputStream(pis);
//      subjectSer = (Subject) ois.readObject();
//      oos.close();
//      ois.close();
//      if (logger.isLoggable(BasicLevel.DEBUG))
//        logger.log(BasicLevel.DEBUG, "serializeSubject subjectSer = " + subjectSer);
//    } catch (Exception e) {
//      if (logger.isLoggable(BasicLevel.ERROR))
//        logger.log(BasicLevel.ERROR, "EXCEPTION:: serializeSubject", e);
//    }
//    return subjectSer;
//  }
  
  public String toString() {
    StringBuffer buff = new StringBuffer();
    buff.append("JonasIdentity (");
    buff.append("principal=");
    buff.append(principal);
    buff.append(",subject=");
    buff.append(subject);
    buff.append(')');
    return buff.toString();
  }
  
  /* (non-Javadoc)
   * @see org.objectweb.joram.shared.stream.Streamable#readFrom(java.io.InputStream)
   */
  public void readFrom(InputStream is) throws IOException {
    principal = StreamUtil.readStringFrom(is);
    ByteArrayInputStream bais = null;
    ObjectInputStream ois = null;
    try {
      byte[] subByte = StreamUtil.readByteArrayFrom(is);
      bais = new ByteArrayInputStream(subByte);
      ois = new ObjectInputStream(bais);
      try {
        subject = (Subject) ois.readObject();
      } catch (ClassNotFoundException e) {
        if (logger.isLoggable(BasicLevel.ERROR))
          logger.log(BasicLevel.ERROR, "EXCEPTION:: readFrom", e);
        throw new IOException(e.getMessage());
      }
    } finally {
      try {
        ois.close();
      } catch (IOException exc) {}
      try {
        bais.close();
      } catch (IOException exc) {}
    }
  }

  /* (non-Javadoc)
   * @see org.objectweb.joram.shared.stream.Streamable#writeTo(java.io.OutputStream)
   */
  public void writeTo(OutputStream os) throws IOException {
    StreamUtil.writeTo(principal, os);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    try {
      oos.writeObject(subject);
      oos.flush();
      StreamUtil.writeTo(baos.toByteArray(), os);
      oos.close();
      baos.close();
    } finally {
      try {
        oos.close();
      } catch (IOException exc) {}
      try {
        baos.close();
      } catch (IOException exc) {}
    }
  }
}
