/********************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ********************************************************************/
package gov.va.med.fw.security;

import java.io.IOException;
import java.net.URL;
import java.security.Principal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import org.acegisecurity.AcegiSecurityException;
import org.acegisecurity.Authentication;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.providers.AuthenticationProvider;
import org.acegisecurity.providers.jaas.JaasAuthenticationCallbackHandler;
import org.acegisecurity.providers.jaas.JaasAuthenticationProvider;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContextAware;

/** 
 * NOTE: This class is not located in the jaas package as it needed to access
 * the package protected method of AuthenticationToken (setLoggedIn) in this package.
 *  
 * AuthenticationProvider implementation for the Jaas provider.
 * Extends from the Acegi's Jaas implementation (JaasAuthenticationProvider) 
 * and override the supports and authenticate method. 
 * Supports AuthenticationToken Object only.
 * 
 * On successful authentication, it sets the logged in flag in AuthenticationToken
 * to be used as a logon flag later on.
 * By default, user name is also added in the granted authority list, so that 
 * page/method permissions could be assigned to user as well.
 * 
 * A configurable allowUserAsAuthority property allows to configure this behaviour. 
 * 
 *  
 * @author DNS   MANSOG
 * @date Apr 28, 2005 6:35:07 PM
 */


public class JaasAuthenticationProviderImpl extends JaasAuthenticationProvider implements AuthenticationProvider,
        InitializingBean, ApplicationContextAware {
    private String loginConfigFile = "/jaas.login.conf";
    //add the user to authority list only when this value is true (default)
    private boolean allowUserAsAuthority = true;
    
    /**
     * Whether to allow the user as a authority
     */
    public boolean isAllowUserAsAuthority() {
        return allowUserAsAuthority;
    }
    /**
     * Set whether is user is allowed to be the part of authority list or not.
     * If false, user name will not be added in the authority list.
     * 
     * @param allowUserAsAuthority
     */
    public void setAllowUserAsAuthority(boolean allowUserAsAuthority) {
        this.allowUserAsAuthority = allowUserAsAuthority;
    }

    
    public String getLoginConfigFile() {
        return loginConfigFile;
    }
    public void setLoginConfigFile(String loginConfigFile) {
        this.loginConfigFile = loginConfigFile;
    }
    public void afterPropertiesSet() throws Exception {
        //super.afterPropertiesSet();
        URL confFileUrl = getClass().getResource(loginConfigFile);
        //System.out.println("Replacing property value "+System.getProperty("java.security.auth.login.config"));
        System.setProperty("java.security.auth.login.config", confFileUrl.toString());
    }
    
    public Authentication authenticate(Authentication auth) throws AuthenticationException {

    	//Process ESR specific token only
    	if (auth instanceof AuthenticationToken) {
            AuthenticationToken authToken = (AuthenticationToken) auth;
            
            // already logged in, return without doing anything.
            if (authToken.isLoggedIn()) {
                return auth;
            }
                                  
	        try {
	            //Create the LoginContext object, and pass our InternallCallbackHandler
	            LoginContext loginContext = new LoginContext(getLoginContextName(), new InternalCallbackHandler(auth));
	            
	            //Attempt to login the user, the LoginContext will call our InternalCallbackHandler at this point.
	            loginContext.login();
	
	            //create a set to hold the authorities, and add any that have already been applied.
	            Set authorities = new HashSet();
	
	            if (authToken.getAuthorities() != null) {
	                authorities.addAll(Arrays.asList(authToken.getAuthorities()));
	            }

	            //get the subject principals and pass them to each of the AuthorityGranters
	            Set principals = loginContext.getSubject().getPrincipals();

	            for (Iterator iterator = principals.iterator(); iterator.hasNext();) {
	                Principal principal = (Principal) iterator.next();
	
	                /*for (int i = 0; i < authorityGranters.length; i++) {
	                    AuthorityGranter granter = authorityGranters[i];
	                    Set roles = granter.grant(principal);
	
	                    //If the granter doesn't wish to grant any authorities, it should return null.
	                    if ((roles != null) && !roles.isEmpty()) {
	                        for (Iterator roleIterator = roles.iterator(); roleIterator.hasNext();) {
	                            String role = roleIterator.next().toString();
	                            authorities.add(new JaasGrantedAuthority(role, principal));
	                        }
	                    }
	                }*/
	                //Add all authorities from the user principal
	                if (principal instanceof UserPrincipal) {
	                    UserPrincipal userPrincipal = (UserPrincipal) principal;
	                    //set the UserPrincipal in the authentication token
	                    authToken.setUserPrincipal(userPrincipal);
	                    if (allowUserAsAuthority) {
	                        authorities.add(new GrantedAuthorityImpl(userPrincipal.getName()));
	                    }
	                    
	                    authorities.addAll(userPrincipal.getAuthorities());
	                }
	            }
	            
	            AuthenticationToken result = 
	            	new AuthenticationToken(authToken.getPrincipal(),authToken.getCredentials(),
	            	(GrantedAuthority[]) authorities.toArray(new GrantedAuthority[authorities.size()]));
	            result.setUserPrincipal(authToken.getUserPrincipal());
	            result.setLoggedIn(true);
   
	            //Publish the success event
	            publishSuccessEvent(result);

	            //we're done, return the token.
	            return result;
	        } catch (LoginException loginException) {
	        	authToken.setLoggedIn(false);
	            AcegiSecurityException ase = getLoginExceptionResolver().resolveException(loginException);
	            publishFailureEvent(authToken, ase);
	            throw ase;
	        }
	    }
	
    	return null; //we are not processing anyother type of tokens
    }    
    /* 
     * Override the authenticate method of JaasAuthenticationProvider.
     * If loggedIn flag is on, this method returns the same Authentication Object
     *  without doing anything.
     * 
     * @see net.sf.acegisecurity.providers.AuthenticationProvider#authenticate(net.sf.acegisecurity.Authentication)
     */
    public Authentication authenticateOld(Authentication auth)
            throws AuthenticationException {

        if (auth instanceof AuthenticationToken) {
            AuthenticationToken authToken = (AuthenticationToken) auth;
            if (authToken.isLoggedIn()) {
                // already logged in, return without doing anything.
                return auth;
            }
            
            try {
                //Create the LoginContext object, and pass our
                // InternallCallbackHandler
                LoginContext lc = new LoginContext(getLoginContextName(),
                        new InternalCallbackHandler(auth));

                //Attempt to login the user, the LoginContext will call our
                // InternalCallbackHandler at this point.
                lc.login();
                
                successfulJaasAuthentication(lc, authToken);

                //we're done, return the token.
                return authToken;
            } catch (LoginException loginException) {
                authToken.setLoggedIn(false);
                authToken.setAuthenticated(false);

                AcegiSecurityException ase = getLoginExceptionResolver()
                        .resolveException(loginException);
                
                publishFailureEvent(authToken, ase);
                throw ase;
            }
        }

        return null;
    }

    /**
     * Called on successful authentication.
     * 
     * @param lc
     * @param authToken
     */
    private void successfulJaasAuthentication(LoginContext lc, AuthenticationToken authToken) {
    	//create a set to hold the authorities, and add any that have
        // already been applied.
        Set authorities = new HashSet();

        if (authToken.getAuthorities() != null) {
            authorities.addAll(Arrays.asList(authToken.getAuthorities()));
        }

        //get the subject principals and pass them to each of the
        // AuthorityGranters
        Set principals = lc.getSubject().getPrincipals();

        for (Iterator iterator = principals.iterator(); iterator
                .hasNext();) {
            Principal principal = (Principal) iterator.next();
            if (principal instanceof UserPrincipal) {
                UserPrincipal userPrincipal = (UserPrincipal) principal;
                //set the UserPrincipal in the authentication token
                authToken.setUserPrincipal(userPrincipal);
                if (allowUserAsAuthority) {
                    authorities.add(new GrantedAuthorityImpl(userPrincipal.getName()));
                }
                
                authorities.addAll(userPrincipal.getAuthorities());
            }
        }
        
        //set the authenticated values to true.
        authToken.setLoggedIn(true);
        //authToken.setAuthenticated(true);
        

        //Convert the authorities set back to an array and apply it to
        // the token.
        /*????authToken.setAuthorities((GrantedAuthority[]) authorities
                .toArray(new GrantedAuthority[authorities.size()]));*/
        
        //Publish the success event
        publishSuccessEvent(authToken);    	
    }

    /* 
     * Supperst the AuthenticationToken Object only.
     * @see net.sf.acegisecurity.providers.AuthenticationProvider#supports(java.lang.Class)
     */
    public boolean supports(Class aClass) {
        return AuthenticationToken.class
                .isAssignableFrom(aClass);
    }
    /**
     * Wrapper class for JAASAuthenticationCallbackHandlers
     */
    private class InternalCallbackHandler implements CallbackHandler {
        private Authentication authentication;

        public InternalCallbackHandler(Authentication authentication) {
            this.authentication = authentication;
        }

        public void handle(Callback[] callbacks) throws IOException,
                UnsupportedCallbackException {
            JaasAuthenticationCallbackHandler[] callbackHandlers = getCallbackHandlers();
            for (int i = 0; i < callbackHandlers.length; i++) {
                JaasAuthenticationCallbackHandler handler = callbackHandlers[i];

                for (int j = 0; j < callbacks.length; j++) {
                    Callback callback = callbacks[j];

                    handler.handle(callback, authentication);
                }
            }
        }
    }
}