/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (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.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
package com.arsdigita.kernel.security;

import java.io.IOException;
import java.util.Map;
import javax.security.auth.Subject;
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 javax.security.auth.spi.LoginModule;
import org.apache.log4j.Logger;

/**
 * Superclass of LoginModules that authenticate users using a username and
 * password.  Fetches the username/password from shared data if possible,
 * otherwise queries the user directly using callbacks.  Saves the
 * username/password in shared data for use by other LoginModules.
 *
 * @author Sameer Ajmani
 **/
public abstract class PasswordLoginModule implements LoginModule {

    public static final String versionId = "$Id: //core-platform/dev/src/com/arsdigita/kernel/security/PasswordLoginModule.java#9 $ by $Author: dennis $, $DateTime: 2004/04/07 16:07:11 $";
    private static final Logger s_log =
        Logger.getLogger(PasswordLoginModule.class.getName());

    /** Key for username in shared data map. **/
    public static final String NAME_KEY
        = "javax.security.auth.login.name";
    /** Key for password in shared data map. **/
    public static final String PASSWORD_KEY
        = "javax.security.auth.login.password";

    // fields set by initialize()
    private Subject m_subject;
    private CallbackHandler m_handler;
    private Map m_shared;
    private Map m_options;

    // implements LoginModule
    public void initialize(Subject subject,
                           CallbackHandler handler,
                           Map shared,
                           Map options) {
        m_subject = subject;
        m_handler = handler;
        m_shared = shared;
        m_options = options;
        // TODO: support "debug" option
    }

    /**
     * Retreives the username and password and calls the
     * <code>checkPassword</code> method.
     *
     * @return <code>true</code>.
     *
     * @throws LoginException if an error occurs.  Propagates exceptions
     * thrown by the <code>checkPassword</code> method.
     *
     * @see #checkPassword(String, char[])
     **/
    public boolean login() throws LoginException {
        s_log.debug("START login");
        checkPassword(getUsername(), getPassword());
        s_log.debug("SUCCESS login");
        return true;
    }

    /**
     * Attempts to read username from shared data map; otherwise retreives
     * it using a NameCallback.
     *
     * @return the username.
     *
     * @throws LoginException if an error occurs.
     **/
    private String getUsername() throws LoginException {
        // get name from shared data
        // TODO: only if *Pass option set
        String username = (String)m_shared.get(NAME_KEY);
        if (username != null) {
            return username;
        }
        // get name using callback and save in shared data
        try {
            NameCallback cb = new NameCallback("Username: ");
            m_handler.handle(new Callback[] {cb});
            username = cb.getName();
            m_shared.put(NAME_KEY, username);
            return username;
        } catch (UnsupportedCallbackException e) {
            throw new KernelLoginException("Could not get username", e);
        } catch (IOException e) {
            throw new KernelLoginException("Could not get username", e);
        }
    }

    /**
     * Attempts to read password from shared data map; otherwise retreives
     * it using a PasswordCallback.
     *
     * @return the password.
     *
     * @throws LoginException if an error occurs.
     **/
    private char[] getPassword() throws LoginException {
        // get password from shared data
        // TODO: only if *Pass option set
        char[] password = (char[])m_shared.get(PASSWORD_KEY);
        if (password != null) {
            return password;
        }
        // get password using callback and save in shared data
        try {
            PasswordCallback cb = new PasswordCallback("Password: ",
                                                       false);
            m_handler.handle(new Callback[] {cb});
            password =  cb.getPassword();
            m_shared.put(PASSWORD_KEY, password);
            return password;
        } catch (UnsupportedCallbackException e) {
            throw new KernelLoginException("Could not get password", e);
        } catch (IOException e) {
            throw new KernelLoginException("Could not get password", e);
        }
    }

    /**
     * Checks whether the given username/password combination is valid.
     *
     * @param username the username to check
     * @param password the password to check
     *
     * @throws AccountNotFoundException if the account does not exist.
     * @throws AccountExpiredException if the account has expired.
     * @throws AccountLockedException if the account is locked.
     * @throws FailedLoginException if the password is invalid.
     * @throws LoginException if an error occurs.
     **/
    protected abstract void checkPassword
        (String username, char[] password) throws LoginException;

    // implements LoginModule
    public abstract boolean commit() throws LoginException;

    // implements LoginModule
    public abstract boolean abort() throws LoginException;

    // implements LoginModule
    public abstract boolean logout() throws LoginException;
}
