//
//  ========================================================================
//  Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.jaas;

import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;

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.LoginContext;
import javax.security.auth.login.LoginException;

import org.eclipse.jetty.jaas.callback.ObjectCallback;
import org.eclipse.jetty.jaas.callback.RequestParameterCallback;
import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/* ---------------------------------------------------- */
/** JAASLoginService
 *
 * @org.apache.xbean.XBean element="jaasUserRealm" description="Creates a UserRealm suitable for use with JAAS"
 */
public class JAASLoginService extends AbstractLifeCycle implements LoginService
{
    private static final Logger LOG = Log.getLogger(JAASLoginService.class);

    public static final String DEFAULT_ROLE_CLASS_NAME = "org.eclipse.jetty.jaas.JAASRole";
    public static final String[] DEFAULT_ROLE_CLASS_NAMES = {DEFAULT_ROLE_CLASS_NAME};

    protected String[] _roleClassNames = DEFAULT_ROLE_CLASS_NAMES;
    protected String _callbackHandlerClass;
    protected String _realmName;
    protected String _loginModuleName;
    protected JAASUserPrincipal _defaultUser = new JAASUserPrincipal(null, null, null);
    protected IdentityService _identityService;

    /* ---------------------------------------------------- */
    /**
     * Constructor.
     *
     */
    public JAASLoginService()
    {
    }


    /* ---------------------------------------------------- */
    /**
     * Constructor.
     *
     * @param name the name of the realm
     */
    public JAASLoginService(String name)
    {
        this();
        _realmName = name;
        _loginModuleName = name;
    }


    /* ---------------------------------------------------- */
    /**
     * Get the name of the realm.
     *
     * @return name or null if not set.
     */
    public String getName()
    {
        return _realmName;
    }


    /* ---------------------------------------------------- */
    /**
     * Set the name of the realm
     *
     * @param name a <code>String</code> value
     */
    public void setName (String name)
    {
        _realmName = name;
    }

    /* ------------------------------------------------------------ */
    /** Get the identityService.
     * @return the identityService
     */
    public IdentityService getIdentityService()
    {
        return _identityService;
    }

    /* ------------------------------------------------------------ */
    /** Set the identityService.
     * @param identityService the identityService to set
     */
    public void setIdentityService(IdentityService identityService)
    {
        _identityService = identityService;
    }

    /* ------------------------------------------------------------ */
    /**
     * Set the name to use to index into the config
     * file of LoginModules.
     *
     * @param name a <code>String</code> value
     */
    public void setLoginModuleName (String name)
    {
        _loginModuleName = name;
    }

    /* ------------------------------------------------------------ */
    public void setCallbackHandlerClass (String classname)
    {
        _callbackHandlerClass = classname;
    }

    /* ------------------------------------------------------------ */
    public void setRoleClassNames (String[] classnames)
    {
        ArrayList<String> tmp = new ArrayList<String>();

        if (classnames != null)
            tmp.addAll(Arrays.asList(classnames));

        if (!tmp.contains(DEFAULT_ROLE_CLASS_NAME))
            tmp.add(DEFAULT_ROLE_CLASS_NAME);
        _roleClassNames = tmp.toArray(new String[tmp.size()]);
    }

    /* ------------------------------------------------------------ */
    public String[] getRoleClassNames()
    {
        return _roleClassNames;
    }

    /* ------------------------------------------------------------ */
    /**
     * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
     */
    protected void doStart() throws Exception
    {
        if (_identityService==null)
            _identityService=new DefaultIdentityService();
        super.doStart();
    }

    /* ------------------------------------------------------------ */
    public UserIdentity login(final String username,final Object credentials)
    {
        try
        {
            CallbackHandler callbackHandler = null;


            if (_callbackHandlerClass == null)
            {
                callbackHandler = new CallbackHandler()
                {
                    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException
                    {
                        for (Callback callback: callbacks)
                        {
                            if (callback instanceof NameCallback)
                            {
                                ((NameCallback)callback).setName(username);
                            }
                            else if (callback instanceof PasswordCallback)
                            {
                                ((PasswordCallback)callback).setPassword((char[]) credentials.toString().toCharArray());
                            }
                            else if (callback instanceof ObjectCallback)
                            {
                                ((ObjectCallback)callback).setObject(credentials);
                            }
                            else if (callback instanceof RequestParameterCallback)
                            {
                                HttpChannel channel = HttpChannel.getCurrentHttpChannel();

                                if (channel == null)
                                    return;
                                Request request = channel.getRequest();

                                if (request != null)
                                {
                                    RequestParameterCallback rpc = (RequestParameterCallback)callback;
                                    rpc.setParameterValues(Arrays.asList(request.getParameterValues(rpc.getParameterName())));
                                }
                            }
                            else
                                throw new UnsupportedCallbackException(callback);
                        }
                    }
                };
            }
            else
            {
                Class clazz = Loader.loadClass(getClass(), _callbackHandlerClass);
                callbackHandler = (CallbackHandler)clazz.newInstance();
            }
            //set up the login context
            //TODO jaspi requires we provide the Configuration parameter
            Subject subject = new Subject();
            LoginContext loginContext = new LoginContext(_loginModuleName, subject, callbackHandler);

            loginContext.login();

            //login success
            JAASUserPrincipal userPrincipal = new JAASUserPrincipal(getUserName(callbackHandler), subject, loginContext);
            subject.getPrincipals().add(userPrincipal);

            return _identityService.newUserIdentity(subject,userPrincipal,getGroups(subject));
        }
        catch (LoginException e)
        {
            LOG.warn(e);
        }
        catch (IOException e)
        {
            LOG.warn(e);
        }
        catch (UnsupportedCallbackException e)
        {
           LOG.warn(e);
        }
        catch (InstantiationException e)
        {
            LOG.warn(e);
        }
        catch (IllegalAccessException e)
        {
            LOG.warn(e);
        }
        catch (ClassNotFoundException e)
        {
            LOG.warn(e);
        }
        return null;
    }

    /* ------------------------------------------------------------ */
    public boolean validate(UserIdentity user)
    {
        // TODO optionally check user is still valid
        return true;
    }

    /* ------------------------------------------------------------ */
    private String getUserName(CallbackHandler callbackHandler) throws IOException, UnsupportedCallbackException
    {
        NameCallback nameCallback = new NameCallback("foo");
        callbackHandler.handle(new Callback[] {nameCallback});
        return nameCallback.getName();
    }

    /* ------------------------------------------------------------ */
    public void logout(UserIdentity user)
    {
        Set<JAASUserPrincipal> userPrincipals = user.getSubject().getPrincipals(JAASUserPrincipal.class);
        LoginContext loginContext = userPrincipals.iterator().next().getLoginContext();
        try
        {
            loginContext.logout();
        }
        catch (LoginException e)
        {
            LOG.warn(e);
        }
    }


    /* ------------------------------------------------------------ */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private String[] getGroups (Subject subject)
    {
        //get all the roles of the various types
        String[] roleClassNames = getRoleClassNames();
        Collection<String> groups = new LinkedHashSet<String>();
        try
        {
            for (String roleClassName : roleClassNames)
            {
                Class load_class = Thread.currentThread().getContextClassLoader().loadClass(roleClassName);
                Set<Principal> rolesForType = subject.getPrincipals(load_class);
                for (Principal principal : rolesForType)
                {
                    groups.add(principal.getName());
                }
            }

            return groups.toArray(new String[groups.size()]);
        }
        catch (ClassNotFoundException e)
        {
            throw new RuntimeException(e);
        }
    }

}
