/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, 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.teiid.jboss;

import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.acl.Group;
import java.util.HashMap;
import java.util.Map;

import javax.resource.spi.security.PasswordCredential;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;

import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import org.jboss.security.SecurityContextAssociation;
import org.jboss.security.SimplePrincipal;
import org.jboss.security.vault.SecurityVaultException;
import org.jboss.security.vault.SecurityVaultUtil;
import org.picketbox.datasource.security.AbstractPasswordCredentialLoginModule;
import org.teiid.OAuthCredentialContext;

/**
 * A simple login module passes the principal making the connection request 
 * to EIS, as pass-through without any validation.
 * 
 */

@SuppressWarnings("unchecked")
public class PassthroughIdentityLoginModule extends AbstractPasswordCredentialLoginModule {
   /**
    * Module option to specify if any {@link GSSCredential} being added to the
    * {@link Subject} should be wrapped to prevent disposal.
    *
    * Has no effect if a {@link GSSCredential} is not being added to the
    * {@link Subject}.
    *
    * Defaults to false.
    */
   private static final String WRAP_GSS_CREDENTIAL = "wrapGSSCredential";    
   private String userName;
   private char[] password;
   private Subject callerSubject;
   private boolean addPrincipal = true;
   private HashMap<String, Object> properties = new HashMap<String, Object>();
   private boolean wrapGssCredential;
   private Subject intermediateSubject;
   private GSSCredential storedCredential;
   
   @Override
   public void initialize(Subject subject, CallbackHandler handler, Map<String, ?> sharedState, Map<String, ?> options) {
      super.initialize(subject, handler, sharedState, options);

      this.userName = (String) options.get("username"); //$NON-NLS-1$

      String pass = (String) options.get("password");//$NON-NLS-1$
        if (pass != null) {
            if (SecurityVaultUtil.isVaultFormat(pass)) {
                try {
                    pass = SecurityVaultUtil.getValueAsString(pass);
                } catch (SecurityVaultException e) {
                    throw new RuntimeException(e);
                }
                this.password = pass.toCharArray();
            } else {
                this.password = pass.toCharArray();
            }
        }
        this.properties.putAll(options);
        this.wrapGssCredential = Boolean.parseBoolean((String) options.get(WRAP_GSS_CREDENTIAL));
        log.tracef("wrapGssCredential=%b", wrapGssCredential);        
   }

    @Override
    public boolean login() throws LoginException {

        String username = userName;
        try {
            Principal user = getPrincipal();
            this.callerSubject = getSubject();
            this.addPrincipal = false;
            
            if (user != null) {
                username = user.getName();
            }            
        } catch (Throwable e) {
            throw new LoginException(e.getMessage());
        }

        // Update userName so that getIdentity is consistent
        this.userName = username;
        if (super.login() == true) {
            return true;
        }

        // Put the principal name into the sharedState map
        sharedState.put("javax.security.auth.login.name", username); //$NON-NLS-1$
        super.loginOk = true;
        return true;
   }

   @Override
   public boolean commit() throws LoginException {
      // Put the principal name into the sharedState map
      sharedState.put("javax.security.auth.login.name", userName); //$NON-NLS-1$
      
      if (this.addPrincipal) {
          subject.getPrincipals().add(getIdentity());
          
          // Add the PasswordCredential
          if (this.password != null) {
              PasswordCredential cred = new PasswordCredential(userName, password);
              SecurityActions.addCredentials(subject, cred);
          }          
      }

      if (this.callerSubject != null) {
          GSSCredential rawCredential = getGssCredential(this.callerSubject);
          if (rawCredential != null) {
              log.trace("Kerberos passthough mechanism in works");
              this.storedCredential = wrapGssCredential ? wrapCredential(rawCredential) : rawCredential;
              this.intermediateSubject = GSSUtil.createGssSubject(rawCredential, storedCredential);
              log.tracef("created a subject from deletegate credential");
              makeCopy(intermediateSubject, this.subject);
              log.tracef("Copied contents of temporary Subject to Subject from the LoginContext"); 
              addPrivateCredential(this.subject, storedCredential);
              log.trace("Also add the GSSCredential to the Subject");
          } else {
              makeCopy(this.callerSubject, this.subject);
          }          
      }

      addPrivateCredential(this.subject, this.properties);
      log.trace("Adding module option properties as private credential");
      
      // if oauth credential available in calling context then add the OAuthCredential.
      if (OAuthCredentialContext.getCredential() != null) {
          addPrivateCredential(this.subject, OAuthCredentialContext.getCredential());
          log.trace("Adding OAuth credential as private credential");
      }
      return true;
   }
   
   @Override
   public boolean logout() throws LoginException {
       if (System.getSecurityManager() == null) {
           if (storedCredential != null) {
               removePrivateCredential(subject, storedCredential);
               log.trace("Remove GSSCredential to the Subject");
           }
           removePrivateCredential(subject, properties);
           clearSubjectContents(subject, intermediateSubject != null?intermediateSubject:callerSubject);
           log.trace("Clear Subject contents");
           return true;
       }
       
       return AccessController.doPrivileged(new PrivilegedAction<Boolean>() { 
           public Boolean run() {
               if (storedCredential != null) {
                   removePrivateCredential(subject, storedCredential);
                   log.trace("Remove GSSCredential to the Subject");
               }
               removePrivateCredential(subject, properties);               
               clearSubjectContents(subject, intermediateSubject != null?intermediateSubject:callerSubject);
               log.trace("Clear Subject contents");
               return true;
           }
       });  
   }
   
   private GSSCredential getGssCredential(Subject subject) {
       for(Object obj:subject.getPrivateCredentials()) {
           if (obj instanceof GSSCredential) {
               return (GSSCredential)obj;
           }
       }
       return null;
   }

   void clearSubjectContents(Subject toSubtract, Subject from) {
       from.getPrincipals().removeAll(toSubtract.getPrincipals());
       from.getPublicCredentials().removeAll(toSubtract.getPublicCredentials());
       from.getPrivateCredentials().removeAll(toSubtract.getPrivateCredentials());
   }
   
   @Override
   protected Principal getIdentity() {
      Principal principal = new SimplePrincipal(userName);
      return principal;
   }   
   
   @Override
   protected Group[] getRoleSets() throws LoginException {
       return new Group[]{};    
   }

   static Principal getPrincipal() {
       if (System.getSecurityManager() == null) {
           return SecurityContextAssociation.getPrincipal();
       }
       
       return AccessController.doPrivileged(new PrivilegedAction<Principal>() { 
           public Principal run() {
               return SecurityContextAssociation.getPrincipal();
           }
       });        
   } 
   
   static Subject getSubject() {
       if (System.getSecurityManager() == null) {
           return SecurityContextAssociation.getSubject();
       }
       
       return AccessController.doPrivileged(new PrivilegedAction<Subject>() { 
           public Subject run() {
               return SecurityContextAssociation.getSubject();
           }
       });        
   }    
   
   static Object makeCopy(final Subject from, final Subject to) {
       if (System.getSecurityManager() == null) {
           copy(from, to);
           return null;
       }
       
       return AccessController.doPrivileged(new PrivilegedAction<Object>() { 
           public Object run() {
               copy(from, to);
               return null;
           }
       });        
   }   
   
   static void copy (final Subject from, final Subject to) {
       for(Principal p:from.getPrincipals()) {
           to.getPrincipals().add(p);
       }
       
       for (Object obj: from.getPrivateCredentials()) {
           to.getPrivateCredentials().add(obj);
       }
       
       for (Object obj: from.getPublicCredentials()) {
           to.getPublicCredentials().add(obj);
       }
   }
   
   static void addPrivateCredential(final Subject subject, final Object obj) {
       if (System.getSecurityManager() == null) {
           subject.getPrivateCredentials().add(obj);
       }
       else {
       AccessController.doPrivileged(new PrivilegedAction<Object>() { 
           public Object run() {
               subject.getPrivateCredentials().add(obj);
               return null;
           }
       });   
       }
   }
   
   static void removePrivateCredential(final Subject subject, final Object obj) {
       if (System.getSecurityManager() == null) {
           subject.getPrivateCredentials().remove(obj);
       }
       else {
       AccessController.doPrivileged(new PrivilegedAction<Object>() { 
           public Object run() {
               subject.getPrivateCredentials().remove(obj);
               return null;
           }
       });   
       }
   }   
   
   private static GSSCredential wrapCredential(final GSSCredential credential) {
       return new GSSCredential() {

           @Override
           public int getUsage(Oid mech) throws GSSException {
               return credential.getUsage(mech);
           }

           @Override
           public int getUsage() throws GSSException {
               return credential.getUsage();
           }

           @Override
           public int getRemainingLifetime() throws GSSException {
               return credential.getRemainingLifetime();
           }

           @Override
           public int getRemainingInitLifetime(Oid mech) throws GSSException {
               return credential.getRemainingInitLifetime(mech);
           }

           @Override
           public int getRemainingAcceptLifetime(Oid mech) throws GSSException {
               return credential.getRemainingAcceptLifetime(mech);
           }

           @Override
           public GSSName getName(Oid mech) throws GSSException {
               return credential.getName(mech);
           }

           @Override
           public GSSName getName() throws GSSException {
               return credential.getName();
           }

           @Override
           public Oid[] getMechs() throws GSSException {
               return credential.getMechs();
           }

           @Override
           public void dispose() throws GSSException {
               // Prevent disposal of our credential.
           }

           @Override
           public void add(GSSName name, int initLifetime, int acceptLifetime, Oid mech, int usage) throws GSSException {
               credential.add(name, initLifetime, acceptLifetime, mech, usage);
           }
       };
   }   
}
