/*******************************************************************************
 * 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.ofbiz.common.login;

import java.util.Map;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.transaction.Transaction;

import org.apache.ofbiz.base.crypto.HashCrypt;
import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.transaction.GenericTransactionException;
import org.apache.ofbiz.entity.transaction.TransactionUtil;
import org.apache.ofbiz.entity.util.EntityQuery;
import org.apache.ofbiz.entity.util.EntityUtilProperties;
import org.apache.ofbiz.service.DispatchContext;

/** LDAP Authentication Services.
 */
public class LdapAuthenticationServices {

    public static final String module = LdapAuthenticationServices.class.getName();

    public static boolean userLogin(DispatchContext ctx, Map<String, ?> context) {
        if (Debug.verboseOn()) Debug.logVerbose("Starting LDAP authentication", module);
        Properties env = UtilProperties.getProperties("jndiLdap");
        String username = (String) context.get("login.username");
        if (username == null) {
            username = (String) context.get("username");
        }
        String password = (String) context.get("login.password");
        if (password == null) {
            password = (String) context.get("password");
        }
        String dn = null;
        Delegator delegator = ctx.getDelegator();
        boolean isServiceAuth = context.get("isServiceAuth") != null && ((Boolean) context.get("isServiceAuth")).booleanValue();
        GenericValue userLogin = null;
        try {
            userLogin = EntityQuery.use(delegator).from("UserLogin").where("userLoginId", username).cache(isServiceAuth).queryOne();
        } catch (GenericEntityException e) {
            Debug.logWarning(e, "", module);
        }
        if (userLogin != null) {
            dn = userLogin.getString("userLdapDn");
        }
        if (UtilValidate.isEmpty(dn)) {
            String dnTemplate = (String) env.get("ldap.dn.template");
            if (dnTemplate != null) {
                dn = dnTemplate.replace("%u", username);
            }
            if (Debug.verboseOn()) Debug.logVerbose("Using DN template: " + dn, module);
        } else {
            if (Debug.verboseOn()) Debug.logVerbose("Using UserLogin.userLdapDn: " + dn, module);
        }
        env.put(Context.SECURITY_PRINCIPAL, dn);
        env.put(Context.SECURITY_CREDENTIALS, password);
        try {
            // Create initial context
            DirContext ldapCtx = new InitialDirContext(env);
            ldapCtx.close();
        } catch (NamingException e) {
            if (Debug.verboseOn()) Debug.logVerbose("LDAP authentication failed: " + e.getMessage(), module);
            return false;
        }
        if (Debug.verboseOn()) Debug.logVerbose("LDAP authentication succeeded", module);
        if (!"true".equals(env.get("ldap.synchronize.passwords"))) {
            return true;
        }
        // Synchronize user's OFBiz password with user's LDAP password
        if (userLogin != null) {
            boolean useEncryption = "true".equals(EntityUtilProperties.getPropertyValue("security", "password.encrypt", delegator));
            String currentPassword = userLogin.getString("currentPassword");
            boolean samePassword;
            if (useEncryption) {
                samePassword = HashCrypt.comparePassword(currentPassword, LoginServices.getHashType(), password);
            } else {
                samePassword = currentPassword.equals(password);
            }
            if (!samePassword) {
                if (Debug.verboseOn()) Debug.logVerbose("Starting password synchronization", module);
                userLogin.set("currentPassword", useEncryption ? HashCrypt.cryptUTF8(LoginServices.getHashType(), null, password) : password, false);
                Transaction parentTx = null;
                boolean beganTransaction = false;
                try {
                    try {
                        parentTx = TransactionUtil.suspend();
                    } catch (GenericTransactionException e) {
                        Debug.logError(e, "Could not suspend transaction: " + e.getMessage(), module);
                    }
                    try {
                        beganTransaction = TransactionUtil.begin();
                        userLogin.store();
                    } catch (GenericEntityException e) {
                        Debug.logError(e, "Error saving UserLogin", module);
                        try {
                            TransactionUtil.rollback(beganTransaction, "Error saving UserLogin", e);
                        } catch (GenericTransactionException e2) {
                            Debug.logError(e2, "Could not rollback nested transaction: " + e2.getMessage(), module);
                        }
                    } finally {
                        try {
                            TransactionUtil.commit(beganTransaction);
                            if (Debug.verboseOn()) Debug.logVerbose("Password synchronized", module);
                        } catch (GenericTransactionException e) {
                            Debug.logError(e, "Could not commit nested transaction: " + e.getMessage(), module);
                        }
                    }
                } finally {
                    if (parentTx != null) {
                        try {
                            TransactionUtil.resume(parentTx);
                            if (Debug.verboseOn()) Debug.logVerbose("Resumed the parent transaction.", module);
                        } catch (GenericTransactionException e) {
                            Debug.logError(e, "Could not resume parent nested transaction: " + e.getMessage(), module);
                        }
                    }
                }
            }
        }
        return true;
    }
}
