/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/tamino/src/urmrealm/org/apache/slide/urm/realm/URMRealm.java,v 1.4 2005/03/02 10:53:36 eckehard Exp $
 * $Revision: 1.4 $
 * $Date: 2005/03/02 10:53:36 $
 *
 * ====================================================================
 *
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * Licensed 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.slide.urm.realm;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.realm.RealmBase;
import org.apache.slide.urm.URMException;
import org.apache.slide.urm.authenticator.URMAuthenticationFailsException;
import org.apache.slide.urm.authenticator.URMAuthenticator;
import org.apache.slide.urm.authenticator.URMAuthenticatorException;
import org.apache.slide.urm.authenticator.URMSubject;
import org.apache.slide.urm.authenticator.rolemanager.URMRole;
import org.apache.slide.urm.common.URMCloseConnectionException;
import org.apache.slide.urm.common.URMConfigurationException;
import org.apache.slide.urm.common.URMConfigurator;
import org.apache.slide.urm.common.URMConstants;
import org.apache.slide.urm.common.URMPrincipal;
import org.apache.slide.urm.common.impl.URMConfiguratorUtil;
import org.apache.slide.urm.common.impl.URMConfiguratorXML;

/**
 * URM Realm. Derives Realm base to 1) hide differences between Tomcat 4 and Realms, 2) get
 * Lifecycle implementation.
 *
 * @author eckehard.hermann@softwareag.com
 * @author dieter.kessler@softwareag.com
 * @author zsolt.sasvari@softwareag.com
 *
 */
public class URMRealm extends RealmBase {
    private static org.apache.log4j.Logger msLogger =
       org.apache.log4j.Logger.getLogger(URMRealm.class.getName());
       
    private URMAuthenticator mAuthenticator = null;
    private boolean     mDomainInUsername    = true;
    private String      mConfigFilePath      = null;

    private PrincipalCache mPrincipalCache = null;
    
    protected String configFile = null;
    protected String principalCacheSize = null;
    protected String principalValidTime = null;
    protected String guestId = null;
    protected String guestPwd = null;
    protected boolean case_sen = true;

    /**
     * Used by Tomcat to set the attribute configFile.
     */
    public void setConfigFile(String configFile) {
        this.configFile = configFile;
    }    
    
    /**
     * Used by Tomcat to set the attribute configFile.
     */
    public String getConfigFile() {
        return configFile;
    }
    
    /**
     * Used by Tomcat to set the attribute principalCacheSize.
    */
    public void setPrincipalCacheSize(String principalCacheSize) {
        this.principalCacheSize = principalCacheSize;
    }
    
    /**
     * Used by Tomcat to set the attribute principalCacheSize.
    */
    public String getPrincipalCacheSize() {
        return this.principalCacheSize;
    }
    
    /**
     * Used by Tomcat to set the attribute principalValidTime.
    */
    public void setPrincipalValidTime(String principalValidTime) {
        this.principalValidTime = principalValidTime;
    }

    /**
     * Used by Tomcat to set the attribute principalValidTime.
    */
    public String getPrincipalValidTime() {
        return this.principalValidTime;
    }
    
    /* (non-Javadoc)
     * @see org.apache.catalina.Lifecycle#start()
     */
    public void start() throws LifecycleException {
        super.start();

        InputStream fis;
        System.out.println("start new realm");
        // if the java system prop was set we will use it ...
        mConfigFilePath = System.getProperty(URMConfiguratorXML.DEFAULT_CONFIG_FILE_PATH);
        // ... if not then the realm attr will be used
        if (mConfigFilePath == null && configFile != null) {
            mConfigFilePath = configFile;
            java.io.File cfp = new java.io.File(mConfigFilePath);
            // ... if the realm attr contains relative path than
            //     it will be prefixed with the 'catalina.home' java system property
            if (!cfp.isAbsolute()) {
                String catalinahome = System.getProperty("catalina.home");
                if (msLogger.isInfoEnabled())
                    msLogger.info("Path of the config file is relative. Using catalina.home="+catalinahome);
                if (catalinahome != null)
                    mConfigFilePath = catalinahome +
                        (mConfigFilePath.charAt(0) == '/' || mConfigFilePath.charAt(0) == '\\' ? "" : File.separator)
                        + mConfigFilePath;
            }
            System.setProperty(URMConfiguratorXML.DEFAULT_CONFIG_FILE_PATH, mConfigFilePath);
        }
        // reading urm xml configurator
        if (msLogger.isInfoEnabled())
            msLogger.info("Reading config file from '"+mConfigFilePath+"'");
        try {
            fis = new FileInputStream(mConfigFilePath);
        } catch (FileNotFoundException e) {
            String err = "Cannot read configuration from '"+mConfigFilePath+"' ("+
                        e.getClass().getName()+": "+e.getMessage()+")";
            msLogger.fatal(err);
            throw new LifecycleException(err);
        }
        try {
            fis.close();
        } catch (IOException e) {
            String err = "Cannot close configuration file '"+mConfigFilePath+"' ("+
                        e.getClass().getName()+": "+e.getMessage()+")";
            msLogger.fatal(err);
            throw new LifecycleException(err);
        }
        URMConfiguratorXML conf = null;
        try {
            conf = URMConfiguratorXML.newConfigfileConfigurator();
        } catch (Exception e) {
            String err = "Creating configurator ("+mConfigFilePath+") failed: '"+e.getMessage()+"'";
            msLogger.fatal(err);
            throw new LifecycleException(err);
        }
        if (conf == null) {
            String err = "Creating configurator from '"+mConfigFilePath+"' failed (returned 'null').";
            msLogger.fatal(err);
            throw new LifecycleException(err);
        }
        // setting 'authdpath' property for ssx if it is neccessery
        try {
            URMConfigurator udbconf = conf.getSubConfigurator("/Authenticator/Attributes");
            if (udbconf != null) {
	
                Properties ssxprops = URMConfiguratorUtil.getNodeAttrsAsProperties(
                                                    udbconf, "Attribute", "name", "value");
                String authdpath = ssxprops != null ? ssxprops.getProperty("authDaemonPath") : null;
                
                // check if user/domain are case sensetive
                String caseS = ssxprops.getProperty("sensitive");
                if (caseS != null) case_sen = new Boolean(caseS).booleanValue(); 
                
                if (authdpath == null) {
                    authdpath = System.getProperty("catalina.home")+"/native/bin/sagssxauthd2";
                    Properties dpathprop = new Properties();
                    dpathprop.setProperty("name", "authDaemonPath");
                    dpathprop.setProperty("value", authdpath);
                    udbconf.addSubConfigurator("authDaemonPath", dpathprop);
                }
                if (msLogger.isInfoEnabled())
                    msLogger.info("Using '"+authdpath+"' path to run authenticator daemon.");
                mAuthenticator = URMAuthenticator.newInstance(conf);
                URMConfiguratorXML.setDefaultConfigurator(conf);                
            }
        } catch (URMException e) {
            e.printStackTrace();
            String err = "Cannot instantiate the authenticator using the '"+mConfigFilePath+
                        "' config file ("+e.getClass().getName()+": "+e.getMessage()+")";
            msLogger.fatal(err);
            throw new LifecycleException(err);
        }
        
        long cachesize = 0, cachetime = 0;
        URMConfigurator attrconf = conf.getSubConfigurator("/Authenticator/Administrator/UserDatabase/Attributes");
        if (attrconf != null) {
            Properties aprops = URMConfiguratorUtil.getNodeAttrsAsProperties(
                                    attrconf, "Attribute", "name", "value");
            String stmp = aprops.getProperty("allCacheSize");
            if (stmp != null && stmp.length() > 0)
                cachesize = Integer.parseInt(stmp);
            stmp = aprops.getProperty("allCacheTime");
            if (stmp != null && stmp.length() > 0)
                cachetime = Integer.parseInt(stmp);
            
            // get guest account
            guestId = aprops.getProperty("guestId"); 
            guestPwd = aprops.getProperty("guestPassword");            
        }
        if (principalCacheSize != null && principalCacheSize.length() > 0)
            cachesize = Integer.parseInt(principalCacheSize);
        if (principalValidTime != null && principalValidTime.length() > 0)
            cachetime = Integer.parseInt(principalValidTime);
        mPrincipalCache = new PrincipalCache(cachesize, cachetime);
        if (msLogger.isInfoEnabled())
            msLogger.info("Principal cache is created (size="+cachesize+",time="+cachetime+").");
    }


    //-- authenticate methods
    // we have to override the RealmBase implementation to avoid calls to
    // getPassword and getPrincipal (which we cannot support)
    
    /* (non-Javadoc)
     * @see org.apache.catalina.Realm#authenticate(java.lang.String, java.lang.String)
     */
    public Principal authenticate(String username, String credentials) {
        return checkPassword(username, credentials);
    }

    /**
     * Not supported - always returns null.
     */
    public Principal authenticate(String username, String clientDigest,
                                    String nOnce, String nc, String cnonce,
                                    String qop, String realm,
                                    String md5a2) {
        return null;
    }

    /**
     * Not supported - always returns null.
     */
    public Principal authenticate(X509Certificate[] arg0) {
        return null;
    }

    
    //--
    
    
    /**
     * Overrides RealmBase implementation because we use URMPrincipal, not GenericPrincipal
     */
    public boolean hasRole(Principal principal, String role) {
        if ((principal != null) && (role != null) &&
                      (principal instanceof URMPrincipal)) {
            Set rset = null;
            if (role.equals(URMConstants.URM_USER_ROLE)) return true;
            try {
                rset = ((URMPrincipal)principal).getPossibleRoles();
            } catch (URMException e) {
                msLogger.error("Checking '"+role+"' role of '"+principal.getName()+"' principal failed("+e.getMessage()+").");
                return false;
            }
            if (rset == null)
                return false;
            if (msLogger.isDebugEnabled()) {
                String rstr = "";
                Iterator ri = rset.iterator();
                if (ri.hasNext()) {
                    rstr = ((URMSubject)ri.next()).getName();
                    while (ri.hasNext()){
                    	URMSubject sub = (URMSubject)ri.next();
                        rstr += "," + sub.getName();
                    }
                }
                msLogger.debug("Checking '"+role+"' role of '"+principal.getName()+"' in the ("+rstr+") set.");
            }
            Iterator riter = rset.iterator();
            while (riter.hasNext()) {
                String rname = ((URMRole)riter.next()).getName();
                if (rname != null && rname.equals(role))
                    return true;
            }
        }
        return false;
    }

    private Principal checkPassword(String username, String credentials) {
        if (msLogger.isDebugEnabled())
            msLogger.debug("Password checking started("+username+").");

        if (username == null)
            return null;

        if (mAuthenticator == null) {
            String err = "Cannot authenticate user since the authenticator object is not initialized.";
            msLogger.fatal(err);
            throw new RuntimeException(err);
        }

        String upper_dom = null;
        String upper_user = null;
        String dom = null;
        String usr = username;
        if (mDomainInUsername) {
            int idx = 0;
            if ((idx = username.lastIndexOf('\\')) > 0) {
                if (!case_sen) {
	            	upper_dom = username.substring(0, idx);
	                // converts the domain to upper case (if it's not done, the domain will be case sensetive)
	                dom = upper_dom.toUpperCase();
	            	upper_user = username.substring(idx + 1);
	                // converts the domain to upper case (if it's not done, the domain will be case sensetive)
	                usr = upper_user.toUpperCase();
	                
                } else {
	                dom = username.substring(0, idx);
	                usr = username.substring(idx + 1);
                }
            }
        }

        URMPrincipal principal = null;
        // caching management
        String key = ((dom != null && dom.length() > 0) ? (dom + "/") : "") + usr;
        if ((principal = mPrincipalCache.get(key, credentials)) != null) {
            if (msLogger.isDebugEnabled())
                msLogger.debug("Password checking of '"+username+"' is successful (from the principal cache).");
            return principal;
        }
        try {
        	
        	if (usr.equals(guestId) && credentials.equals(guestPwd)) {
        		principal = mAuthenticator.authenticateUser();
        	} else {
	            principal = mAuthenticator.authenticateUser(usr,
	                        credentials != null ? credentials.toCharArray() : null,
	                        dom);
        	}
            mPrincipalCache.add(key, principal, credentials);
            if (msLogger.isDebugEnabled())
                msLogger.debug("Password checking of '"+username+"' is successful.");
      
            return principal;
        } catch (URMAuthenticationFailsException e) {
            mPrincipalCache.remove(key);
            msLogger.debug("Password checking of '"+username+"' is unsuccessful("+e.getMessage()+").");
        } catch (URMAuthenticatorException e) {
            String err = "Password checking of '"+username+"' failed("+e.getMessage()+").";
            msLogger.error(err);
            throw new RuntimeException(err);
        } catch (URMConfigurationException e) {
            String err = "Configuration failure occured during the password checking of '"+username+"' ("+e.getMessage()+").";
            msLogger.error(err);
            throw new RuntimeException(err);
        } catch (URMCloseConnectionException e) {
            String err = "Closing conection failed by the password checking of '"+username+"' ("+e.getMessage()+").";
            msLogger.error(err);
        } catch (URMException e) {
            String err = "Guest logon fails("+e.getMessage()+").";
            msLogger.error(err);
        }

        return null;
    }
    
    //-- RealmBase abstract methods

    public String getName() {
        return "URMRealm";
    }

    /** cannot be implemented with URM - must never be called */
    public String getPassword(String username) {
        throw new UnsupportedOperationException();
    }

    /** cannot be implemented with URM - must never be called */
    public Principal getPrincipal(String username) {
        throw new UnsupportedOperationException();
    }
}
