/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2005 Sun Microsystems Inc. All Rights Reserved
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * https://opensso.dev.java.net/public/CDDLv1.0.html or
 * opensso/legal/CDDLv1.0.txt
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at opensso/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * $Id: SMSAuthModule.java,v 1.9 2009/12/11 06:51:37 hengming Exp $
 *
 * Portions Copyrighted 2011-2016 ForgeRock AS.
 */
package com.sun.identity.authentication.internal.server;

import static org.forgerock.openam.ldap.LDAPUtils.rdnValueFromDn;

import com.iplanet.sso.SSOToken;
import com.sun.identity.authentication.internal.AuthPrincipal;
import com.sun.identity.authentication.internal.AuthSubject;
import com.sun.identity.authentication.internal.LoginModule;
import com.sun.identity.authentication.spi.InvalidPasswordException;
import com.sun.identity.authentication.util.ISAuthConstants;
import com.sun.identity.common.DNUtils;
import com.sun.identity.security.AdminDNAction;
import com.sun.identity.security.AdminPasswordAction;
import com.sun.identity.security.AdminTokenAction;
import com.sun.identity.shared.debug.Debug;
import com.sun.identity.shared.encode.Hash;
import com.sun.identity.sm.SMSEntry;
import com.sun.identity.sm.ServiceConfig;
import com.sun.identity.sm.ServiceConfigManager;
import com.sun.identity.sm.ServiceListener;
import com.sun.identity.sm.ServiceSchemaManager;
import java.io.IOException;
import java.security.AccessController;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListMap;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;

import org.forgerock.openam.ldap.LDAPUtils;

/**
 * AM's internal user's authentication module
 */
public class SMSAuthModule implements LoginModule {

    // Static variables
    private static volatile boolean initialized = false;

    private static volatile boolean loadedInternalUsers = false;

    private static volatile boolean registeredCallbackHandler = false;

    private static final Map<String, String> users = new ConcurrentSkipListMap<>(String.CASE_INSENSITIVE_ORDER);

    private static final Map<String, String> userNameToDN = new ConcurrentSkipListMap<>(String.CASE_INSENSITIVE_ORDER);

    private static final Debug debug = Debug.getInstance("amAuthInternalSMModule");

    // Instance variables
    AuthSubject subject;

    String userDN;

    CallbackHandler cb;

    Map sharedState;

    Map options;

    // Constants
    private static final String IDREPO_SERVICE = "sunIdentityRepositoryService";

    private static final String USERS = "users";

    private static final String DN = "dn";

    private static final String PASSWORD = "userPassword";

    public SMSAuthModule() {
        // do nothing
        if (debug.messageEnabled()) {
            debug.message("SMSAuthModule constructor called");
        }
    }

    public void initialize(AuthSubject subject, CallbackHandler cb,
            Map isharedstate, Map ioptions) {
        this.subject = subject;
        this.cb = cb;
        this.sharedState = (isharedstate != null) ? isharedstate
                : Collections.EMPTY_MAP;
        this.options = (ioptions != null) ? ioptions : Collections.EMPTY_MAP;

        if (debug.messageEnabled()) {
            // Copy the shared state and remove password for debugging
            Map ss = new HashMap(sharedState);
            boolean passwordPresent = (ss
                    .remove(ISAuthConstants.SHARED_STATE_PASSWORD) == null) 
                    ? false
                    : true;
            debug.message("SMSAuthModule::initialize called "
                    + "\nPrincipals: "
                    + ((subject != null) ? subject.getPrincipals().toString()
                            : "null")
                    + "\nSharedState: "
                    + ss
                    + "\n"
                    + (passwordPresent ? "<Password Present> "
                            : "<Password Absent>") + "\nOptions: " + options);
        }

        if (!initialized) {
            initialize();
        }
    }
    
    public static void initialize() {
        if (debug.messageEnabled()) {
            debug.message("SMSAuthModule.initialize() Initializing "
                    + "Username and password from serverconfig.xml");
        }
        
        // reset so that internal users set will be reloaded later in time.
        loadedInternalUsers = false;
        
        // initialize caches.
        users.clear();
        userNameToDN.clear();

        // Get internal user names and passwords from serverconfig.xml
        // %%% Might have to get them directory from DSConfigMgr %%%
        // if other than "default" needs to be used
        String name = (String) AccessController
            .doPrivileged(new AdminDNAction());
        String passwd = (String) AccessController
            .doPrivileged(new AdminPasswordAction());
        addUserToCache(name, Hash.hash(passwd));
        if (debug.messageEnabled()) {
            debug.message("SMSAuthModule.initialize() Username "
                    + "serviceconfig.xml: " + name);
        }
        initialized = true;
    }

    public boolean login() throws LoginException {
        // Check if the user is already present
        String username = (String) sharedState
                .get(ISAuthConstants.SHARED_STATE_USERNAME);
        String password = (String) sharedState
                .get(ISAuthConstants.SHARED_STATE_PASSWORD);
        if (debug.messageEnabled()) {
            debug.message("SMSAuthModule::login() From shared state: "
                    + "Username: " + username + " Password: "
                    + ((password == null) ? "<not present>" : "<present>"));
        }

        // Check if we have username and password, if not send callbacks
        if (username == null || password == null) {
            // Request for both username and password
            Callback cbs[] = new Callback[2];
            cbs[0] = new NameCallback("User name: ");
            cbs[1] = new PasswordCallback("Password: ", false);
            try {
                if (debug.messageEnabled()) {
                    debug.message("SMSAuthModule::login() Sending "
                            + "Name & Password Callback");
                }
                cb.handle(cbs);
            } catch (UnsupportedCallbackException e) {
                throw (new LoginException(e.getMessage()));
            } catch (IOException ioe) {
                throw (new LoginException(ioe.getMessage()));
            }
            username = ((NameCallback) cbs[0]).getName();
            char[] passwd = ((PasswordCallback) cbs[1]).getPassword();
            if (passwd != null) {
                password = new String(password);
            }
        }

        // Authenticate the user, return false is username or password is null
        boolean authenticated = false;
        if (username != null && password != null) {
            if (debug.messageEnabled()) {
                debug.message("SMSAuthModule::login() For authentication: "
                        + "Username: " + username + " Password: <present>");
            }
            if (LDAPUtils.isDN(username)) {
                userDN = username;
                username = rdnValueFromDn(username);
            } else {
                userDN = (String) userNameToDN.get(username);
                if (userDN == null && !loadedInternalUsers) {
                    // Load the internal users and try to get userDN
                    loadInternalUsers();
                    userDN = (String) userNameToDN.get(username);
                }
            }
            // Need to make sure userDN is not null, since this
            // be set in the subject
            if (userDN != null) {
                // Get the hashed password for the user
                String hash = (String) users.get(username);
                String cachedUserDN = (String) userNameToDN.get(username);
                if (cachedUserDN != null) {
                    String normalizedUserDN = DNUtils.normalizeDN(userDN);
                    if ((normalizedUserDN == null) || 
                        !normalizedUserDN.equals(DNUtils.
                        normalizeDN(cachedUserDN))) {
                        debug.message("SMSAuthModule::login() Invalid User DN");
                        return false;
                    }
                }
                // Compare the hashed password
                boolean invalidPassword = false;
                if (hash != null && hash.equals(Hash.hash(password))) {
                    if (debug.messageEnabled()) {
                        debug.message("SMSAuthModule::login() Success AuthN");
                    }
                    authenticated = true;
                } else if (!loadedInternalUsers) {
                    // Load the internal users and compare hashed passwords
                    if (debug.messageEnabled()) {
                        debug.message("SMSAuthModule::login() "
                                + "Loading internal users");
                    }
                    loadInternalUsers();
                    cachedUserDN = (String) userNameToDN.get(username);
                    if (cachedUserDN != null) {
                        String normalizedUserDN = DNUtils.normalizeDN(userDN);
                        if ((normalizedUserDN == null) || 
                            !normalizedUserDN.equals(DNUtils.
                            normalizeDN(cachedUserDN))) {
                            if (debug.messageEnabled()) {
                                debug.message("SMSAuthModule::login() "
                                + "Invalid User DN");
                            }
                            return false;
                        }
                    } else {
                        return false;
                    }
                    hash = (String) users.get(username);
                    if (hash != null && hash.equals(Hash.hash(password))) {
                        if (debug.messageEnabled()) {
                            debug.message("SMSAuthModule::login() "
                                    + "Success AuthN");
                        }
                        authenticated = true;
                    } else if (hash != null) {
                        // Password must be invalid
                        invalidPassword = true;
                    }
                } else if (hash != null) {
                    // Password must be invalid
                    invalidPassword = true;
                }
                if (invalidPassword) {
                    throw (new InvalidPasswordException("invalid password",
                            userDN));
                }
            }
        }
        return (authenticated);
    }

    public boolean abort() throws LoginException {
        // do nothing
        return (true);
    }

    public boolean commit() throws LoginException {
        // add username to Subject
        if (debug.messageEnabled()) {
            debug.message("SMSAuthModule::commit() Adding Principal: " + userDN
                    + " to Subject");
        }
        Set principals = subject.getPrincipals();
        if (principals.isEmpty()) {
            principals.add(new AuthPrincipal(userDN));
        }
        return (true);
    }

    public boolean logout() throws LoginException {
        // do nothing
        return (true);
    }

    private static synchronized void loadInternalUsers() {
        if (loadedInternalUsers) {
            return;
        }

        // Get AdminSSOToken
        try {
            SSOToken ssoToken = (SSOToken) AccessController
                    .doPrivileged(AdminTokenAction.getInstance());
            ServiceConfigManager scm = new ServiceConfigManager(IDREPO_SERVICE,
                    ssoToken);
            ServiceSchemaManager ssm = new ServiceSchemaManager(IDREPO_SERVICE,
                    ssoToken);
            ServiceConfig sc = scm.getGlobalConfig(null);
            sc = sc.getSubConfig(USERS);
            for (Iterator items = sc.getSubConfigNames().iterator(); items
                    .hasNext();) {
                String scName = (String) items.next();
                ServiceConfig s = sc.getSubConfig(scName);
                Map attrs = s.getAttributes();
                String name = null;
                Set set = (Set) attrs.get(DN);
                if (set != null && !set.isEmpty()) {
                    name = (String) set.iterator().next();
                    // Add root suffix, if revision is greater than 30
                    if (ssm.getRevisionNumber() >= 30) {
                        // In the case of upgrade the DN will have the suffix
                        // Hence check if it ends with SMS root suffix
                        if (name.toLowerCase().endsWith(
                            SMSEntry.getRootSuffix().toLowerCase())) {
                            // Replace only if the they are different
                            if (!SMSEntry.getRootSuffix().equals(
                                SMSEntry.getAMSdkBaseDN())) {
                                name = name.substring(0, name.length() -
                                    SMSEntry.getRootSuffix().length());
                                name = name + SMSEntry.getAMSdkBaseDN();
                            }
                        } else {
                            name = name + SMSEntry.getAMSdkBaseDN();
                        }
                    }
                }
                String hash = null;
                set = (Set) attrs.get(PASSWORD);
                if (set != null && !set.isEmpty()) {
                    hash = (String) set.iterator().next();
                }
                if (debug.messageEnabled()) {
                    debug.message("SMSAuthModule::loadInternalUsers() "
                            + "Added user: " + name);
                }
                addUserToCache(name, hash);
            }
            loadedInternalUsers = true;
            // Setup listeners
            if (!registeredCallbackHandler) {
                scm.addListener(new SMSAuthModuleListener());
                registeredCallbackHandler = true;
            }
        } catch (Exception e) {
            // Handle the exception
        }
    }

    private static void addUserToCache(String name, String hash) {
        // Add the DN
        users.put(name, hash);
        // Add the "name"
        String username = rdnValueFromDn(name);
        users.put(username, hash);
        userNameToDN.put(username, name);
    }

    // Inner class for receiving SMS notifications
    static class SMSAuthModuleListener implements ServiceListener {

        SMSAuthModuleListener() {
            // Do nothing
            if (debug.messageEnabled()) {
                debug.message("SMSAuthModuleListener::init called");
            }
        }

        public void schemaChanged(String serviceName, String version) {
            // Ignore
            if (debug.messageEnabled()) {
                debug.message("SMSAuthModuleListener::schemaChanged called");
            }
        }

        public void globalConfigChanged(String serviceName, String version,
                String groupName, String serviceComponent, int type) {
            if (debug.messageEnabled()) {
                debug.message("SMSAuthModuleListener::globalConfigChanged");
            }
            if (serviceName.equalsIgnoreCase(IDREPO_SERVICE)) {
                // Force the loading of internal users
                loadedInternalUsers = false;
                users.clear();
            }
        }

        public void organizationConfigChanged(String serviceName,
                String version, String orgName, String groupName,
                String serviceComponent, int type) {
            // Ignore
            if (debug.messageEnabled()) {
                debug.message("SMSAuthModuleListener::orgConfigChanged");
            }
        }
    }
}
