/*
    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.wiki.auth.login;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import java.security.Principal;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;

/**
 * Abstract JAAS {@link javax.security.auth.spi.LoginModule}that implements
 * base functionality. The methods {@link #login()} and {@link #commit()} must
 * be implemented by subclasses. The default implementations of
 * {@link #initialize(Subject, CallbackHandler, Map, Map)}, {@link #abort()} and
 * {@link #logout()} should be sufficient for most purposes.
 * @since 2.3
 */
public abstract class AbstractLoginModule implements LoginModule {

    private static final Logger log = LogManager.getLogger( AbstractLoginModule.class );

    protected CallbackHandler m_handler;
    protected Map< String, ? > m_options;

    /**
     * Implementing classes should add Principals to this collection; these
     * will be added to the principal set when the overall login succeeds.
     * These Principals will be added to the Subject
     * during the {@link #commit()} phase of login.
     */
    protected Collection< Principal > m_principals;

    protected Map< String, ? > m_state;

    protected Subject m_subject;

    protected static final String NULL = "(null)";

    /**
     * Aborts the login; called if the LoginContext's overall authentication
     * failed. (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL
     * LoginModules did not succeed). Specifically, it removes
     * Principals from the Subject that are associated with the
     * individual LoginModule; these will be those contained in
     * {@link #m_principals}.
     * It always returns <code>true</code>.
     * @see javax.security.auth.spi.LoginModule#abort()
     * @return True, always.
     */
    public final boolean abort()
    {
        removePrincipals( m_principals );

        // Clear the principals/principalsToRemove sets
        m_principals.clear();

        return true;
    }

    /**
     * Commits the login. If the overall login method succeeded, adds
     * principals to the Subject's set; generally, these will be the user's
     * actual Principal, plus one or more Role principals. The state of the
     * <code>m_principals</code> member variable is consulted to determine
     * whether to add the principals. If its size is 0 (because the login
     * failed), the login is considered to have failed; in this case,
     * all principals in {@link #m_principals} are removed
     * from the Subject's set. Otherwise, the principals added to
     * <code>m_principals</code> in the {@link #login()} method are added to
     * the Subject's set.
     * @return <code>true</code> if the commit succeeded, or
     *         <code>false</code> if the previous call to {@link #login()}
     *         failed
     * @see javax.security.auth.spi.LoginModule#commit()
     */
    public final boolean commit() {
        if ( succeeded() ) {
            for ( final Principal principal : m_principals ) {
                m_subject.getPrincipals().add( principal );
                if ( log.isDebugEnabled() ) {
                    log.debug("Committed Principal " + principal.getName() );
                }
            }
            return true;
        }

        // If login did not succeed, clean up after ourselves
        removePrincipals( m_principals );

        // Clear the principals/principalsToRemove sets
        m_principals.clear();

        return false;
    }

    /**
     * Initializes the LoginModule with a given <code>Subject</code>,
     * callback handler, options and shared state. In particular, the member
     * variable <code>m_principals</code> is initialized as a blank Set.
     * @see javax.security.auth.spi.LoginModule#initialize(javax.security.auth.Subject,
     *      javax.security.auth.callback.CallbackHandler, java.util.Map, java.util.Map)
     *      
     * @param subject {@inheritDoc}
     * @param callbackHandler {@inheritDoc}
     * @param sharedState {@inheritDoc}
     * @param options {@inheritDoc}
     */
    public final void initialize( final Subject subject, final CallbackHandler callbackHandler, final Map<String,?> sharedState, final Map<String,?> options ) {
        m_principals = new HashSet<>();
        m_subject = subject;
        m_handler = callbackHandler;
        m_state = sharedState;
        m_options = options;
        if ( subject == null ) {
            throw new IllegalStateException( "Subject cannot be null" );
        }
        if ( callbackHandler == null ) {
            throw new IllegalStateException( "Callback handler cannot be null" );
        }
    }

    /**
     * Logs in the user by calling back to the registered CallbackHandler with a
     * series of callbacks. If the login succeeds, this method returns
     * <code>true</code>
     * @return <code>true</code> if the commit succeeded, or
     *         <code>false</code> if this LoginModule should be ignored.
     * @throws LoginException if the authentication fails
     * @see javax.security.auth.spi.LoginModule#login()
     */
    public abstract boolean login() throws LoginException;

    /**
     * Logs the user out. Removes all principals in {@link #m_principals}
     * from the Subject's principal set.
     * @return <code>true</code> if the commit succeeded, or
     *         <code>false</code> if this LoginModule should be ignored
     * @see javax.security.auth.spi.LoginModule#logout()
     */
    public final boolean logout() {
        removePrincipals( m_principals );

        // Clear the principals/principalsToRemove sets
        m_principals.clear();

        return true;
    }

    /**
     * Returns <code>true</code> if the number of principals
     * contained in {@link #m_principals} is non-zero;
     * <code>false</code> otherwise.
     * @return True, if a login has succeeded.
     */
    private boolean succeeded()
    {
        return m_principals.size() > 0;
    }

    /**
     * Removes a specified collection of Principals from the Subject's
     * Principal set.
     * @param principals the principals to remove
     */
    private void removePrincipals( final Collection<Principal> principals ) {
        for ( final Principal principal : principals ) {
            if ( m_subject.getPrincipals().contains( principal ) ) {
                m_subject.getPrincipals().remove( principal );
                if ( log.isDebugEnabled() ) {
                    log.debug("Removed Principal " + principal.getName() );
                }
            }
        }
    }

}
