/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2012 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.tenant.loginmodule;

import org.ow2.jonas.lib.tenant.context.TenantContext;
import org.ow2.jonas.lib.tenant.context.TenantCurrent;

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.security.acl.Group;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * This class is used to add tenantId in the TenantContext during user login
 * It doesn't make any authentication
 * @author Mohammed Boukada
 */
public class TenantIdLoginModule implements LoginModule {

    /**
     * Subject used
     */
    private Subject subject = null;

    /**
     * Name of the principal
     */
    private String principalName = null;

    /**
     * Roles of the principal
     */
    private List principalRoles = null;

    /**
     * TenantId role prefix
     */
    private final String PREFIX = "TenantId";

    /**
     * Super admin role
     */
    private final String SUPER_ADMIN_ROLE = "super-admin";

    /**
     * Initialize this LoginModule. This method is called by the LoginContext
     * after this LoginModule has been instantiated. The purpose of this method
     * is to initialize this LoginModule with the relevant information. If this
     * LoginModule does not understand any of the data stored in sharedState or
     * options parameters, they can be ignored.
     * @param subject the Subject to be authenticated.
     * @param callbackHandler a CallbackHandler for communicating with the end
     *        user (prompting for usernames and passwords, for example).
     * @param sharedState state shared with other configured LoginModules.
     * @param options options specified in the login Configuration for this
     *        particular LoginModule.
     */
    public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
        this.subject = subject;
        principalRoles = new ArrayList();
    }

    /**
     * Method to authenticate a Subject (phase 1). The implementation of this
     * method authenticates a Subject. For example, it may prompt for Subject
     * information such as a username and password and then attempt to verify
     * the password. This method saves the result of the authentication attempt
     * as private state within the LoginModule.
     * @return true if the authentication succeeded, or false if this
     *         LoginModule should be ignored.
     * @throws LoginException if the authentication fails
     */
    public boolean login() throws LoginException {
        // Do nothing (as all will be done in the commit() phase)
        return true;
    }

    /**
     * Method to commit the authentication process (phase 2). This method is
     * called if the LoginContext's overall authentication succeeded (the
     * relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
     * succeeded). If this LoginModule's own authentication attempt succeeded
     * (checked by retrieving the private state saved by the login method), then
     * this method associates relevant Principals and Credentials with the
     * Subject located in the LoginModule. If this LoginModule's own
     * authentication attempted failed, then this method removes/destroys any
     * state that was originally saved.
     * @return true if this method succeeded, or false if this LoginModule
     *         should be ignored.
     * @throws LoginException if the commit fails
     */
    public boolean commit() throws LoginException {
        // Retrieve only principal name (without groups)
        Set principals = subject.getPrincipals(Principal.class);
        Iterator iterator = principals.iterator();
        while (iterator.hasNext()) {
            Principal principal = (Principal) iterator.next();
            if (!(principal instanceof Group)) {
                principalName = principal.getName();
            }
        }

        // No name --> error
        if (principalName == null) {
            throw new LoginException("There was no previous login module. This login module can only be used in addition to another module which perform the authentication.");
        }

        // Retrieve all roles of the user (Roles are members of the Group.class)
        principals = subject.getPrincipals(Group.class);
        iterator = principals.iterator();
        while (iterator.hasNext()) {
            Group group = (Group) iterator.next();
            Enumeration e = group.members();
            while (e.hasMoreElements()) {
                Principal p = (Principal) e.nextElement();
                principalRoles.add(p.getName());
            }
        }

        // Retrieve tenantId from TenantId role
        String tenantId = null;
        try {
            tenantId = getTenantId(principalRoles);
        } catch (Exception e) {
            throw new LoginException("Error when getting tenantId");
        }

        // Set tenantId in the context
        TenantContext ctx = new TenantContext(null, tenantId);
        TenantCurrent.getCurrent().setTenantContext(ctx);
        return true;
    }

    /**
     * Method to abort the authentication process (phase 2). This method is
     * called if the LoginContext's overall authentication failed. (the relevant
     * REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules did not
     * succeed). If this LoginModule's own authentication attempt succeeded
     * (checked by retrieving the private state saved by the login method), then
     * this method cleans up any state that was originally saved.
     * @return true if this method succeeded, or false if this LoginModule
     *         should be ignored.
     * @throws LoginException if the abort fails
     */
    public boolean abort() throws LoginException {
        // Do nothing (as all is done in the commit() phase)
        return true;
    }

    /**
     * Method which logs out a Subject. An implementation of this method might
     * remove/destroy a Subject's Principals and Credentials.
     * @return true if this method succeeded, or false if this LoginModule
     *         should be ignored.
     * @throws LoginException if the logout fails
     */
    public boolean logout() throws LoginException {

        // Set tenantId in the context
        TenantContext ctx = new TenantContext();
        TenantCurrent.getCurrent().setTenantContext(ctx);
        return true;
    }

    /**
     * Gets tenantId
     * @param principalRoles list of principal roles
     * @return tenantId
     * @throws Exception
     */
    private String getTenantId(List principalRoles) throws Exception {
        String tenantId = null;
        for (Object role : principalRoles) {
            String roleName = (String) role;
            // If the user has the role of superAdmin
            if (SUPER_ADMIN_ROLE.equals(roleName)) {
                // then return the default tenantID to have access to all MBeans
                return TenantContext.DEFAULT_TENANT_ID;
            }

            if (roleName.startsWith(PREFIX)) {
                // get the tenantID from the roleName
                // pattern : TenantId[T<id>]
                tenantId = roleName.substring(roleName.indexOf('[') + 1, roleName.indexOf(']'));
            }
        }
        if (tenantId == null) {
            throw new Exception("TenantId role missing");
        }
        return tenantId;
    }
}
