/*
 *  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.qpid.server.security.access;

import org.apache.qpid.server.management.MBeanDescription;
import org.apache.qpid.server.management.AMQManagedObject;
import org.apache.qpid.server.management.MBeanOperation;
import org.apache.qpid.server.management.MBeanInvocationHandlerImpl;
import org.apache.qpid.server.security.auth.database.PrincipalDatabase;
import org.apache.qpid.server.security.auth.sasl.UsernamePrincipal;
import org.apache.log4j.Logger;
import org.apache.commons.configuration.ConfigurationException;

import javax.management.JMException;
import javax.management.remote.JMXPrincipal;
import javax.management.openmbean.TabularData;
import javax.management.openmbean.TabularDataSupport;
import javax.management.openmbean.TabularType;
import javax.management.openmbean.SimpleType;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeDataSupport;
import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.Subject;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.FileOutputStream;
import java.util.Properties;
import java.util.List;
import java.util.Enumeration;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import java.security.Principal;
import java.security.AccessControlContext;
import java.security.AccessController;

/** MBean class for AMQUserManagementMBean. It implements all the management features exposed for managing users. */
@MBeanDescription("User Management Interface")
public class AMQUserManagementMBean extends AMQManagedObject implements UserManagement
{

    private static final Logger _logger = Logger.getLogger(AMQUserManagementMBean.class);

    private PrincipalDatabase _principalDatabase;
    private String _accessFileName;
    private Properties _accessRights;
    //    private File _accessFile;
    private ReentrantLock _accessRightsUpdate = new ReentrantLock();

    // Setup for the TabularType
    static TabularType _userlistDataType; // Datatype for representing User Lists

    static CompositeType _userDataType; // Composite type for representing User
    static String[] _userItemNames = {"Username", "read", "write", "admin"};

    static
    {
        String[] userItemDesc = {"Broker Login username", "Management Console Read Permission",
                                 "Management Console Write Permission", "Management Console Admin Permission"};

        OpenType[] userItemTypes = new OpenType[4]; // User item types.
        userItemTypes[0] = SimpleType.STRING;  // For Username
        userItemTypes[1] = SimpleType.BOOLEAN; // For Rights - Read
        userItemTypes[2] = SimpleType.BOOLEAN; // For Rights - Write
        userItemTypes[3] = SimpleType.BOOLEAN; // For Rights - Admin
        String[] userDataIndex = {_userItemNames[0]};

        try
        {
            _userDataType =
                    new CompositeType("User", "User Data", _userItemNames, userItemDesc, userItemTypes);

            _userlistDataType = new TabularType("Users", "List of users", _userDataType, userDataIndex);
        }
        catch (OpenDataException e)
        {
            _logger.error("Tabular data setup for viewing users incorrect.");
            _userlistDataType = null;
        }
    }


    public AMQUserManagementMBean() throws JMException
    {
        super(UserManagement.class, UserManagement.TYPE);
    }

    public String getObjectInstanceName()
    {
        return UserManagement.TYPE;
    }

    public boolean setPassword(String username, char[] password)
    {
        try
        {
            //delegate password changes to the Principal Database
            return _principalDatabase.updatePassword(new UsernamePrincipal(username), password);
        }
        catch (AccountNotFoundException e)
        {
            _logger.warn("Attempt to set password of non-existant user'" + username + "'");
            return false;
        }
    }

    public boolean setRights(String username, boolean read, boolean write, boolean admin)
    {

        if (_accessRights.get(username) == null)
        {
            // If the user doesn't exist in the user rights file check that they at least have an account.
            if (_principalDatabase.getUser(username) == null)
            {
                return false;
            }
        }

        try
        {

            _accessRightsUpdate.lock();

            // Update the access rights
            if (admin)
            {
                _accessRights.put(username, MBeanInvocationHandlerImpl.ADMIN);
            }
            else
            {
                if (read | write)
                {
                    if (read)
                    {
                        _accessRights.put(username, MBeanInvocationHandlerImpl.READONLY);
                    }
                    if (write)
                    {
                        _accessRights.put(username, MBeanInvocationHandlerImpl.READWRITE);
                    }
                }
                else
                {
                    _accessRights.remove(username);
                }
            }

            saveAccessFile();
        }
        finally
        {
            if (_accessRightsUpdate.isHeldByCurrentThread())
            {
                _accessRightsUpdate.unlock();
            }
        }

        return true;
    }

    public boolean createUser(String username, char[] password, boolean read, boolean write, boolean admin)
    {
        if (_principalDatabase.createPrincipal(new UsernamePrincipal(username), password))
        {
            _accessRights.put(username, "");

            return setRights(username, read, write, admin);
        }

        return false;
    }

    public boolean deleteUser(String username)
    {

        try
        {
            if (_principalDatabase.deletePrincipal(new UsernamePrincipal(username)))
            {
                try
                {
                    _accessRightsUpdate.lock();

                    _accessRights.remove(username);
                    saveAccessFile();
                }
                finally
                {
                    if (_accessRightsUpdate.isHeldByCurrentThread())
                    {
                        _accessRightsUpdate.unlock();
                    }
                }
                return true;
            }
        }
        catch (AccountNotFoundException e)
        {
            _logger.warn("Attempt to delete user (" + username + ") that doesn't exist");
        }

        return false;
    }

    public boolean reloadData()
    {
        try
        {
            try
            {
                loadAccessFile();
            }
            catch (ConfigurationException e)
            {
                _logger.info("Reload failed due to:" + e);
                return false;
            }

            // Reload successful
            return true;
        }
        catch (IOException e)
        {
            _logger.info("Reload failed due to:" + e);
            // Reload unsuccessful
            return false;
        }
    }


    @MBeanOperation(name = "viewUsers", description = "All users with access rights to the system.")
    public TabularData viewUsers()
    {
        // Table of users
        // Username(string), Access rights Read,Write,Admin(bool,bool,bool)

        if (_userlistDataType == null)
        {
            _logger.warn("TabluarData not setup correctly");
            return null;
        }

        List<Principal> users = _principalDatabase.getUsers();

        TabularDataSupport userList = new TabularDataSupport(_userlistDataType);

        try
        {
            // Create the tabular list of message header contents
            for (Principal user : users)
            {
                // Create header attributes list

                String rights = (String) _accessRights.get(user.getName());

                Boolean read = false;
                Boolean write = false;
                Boolean admin = false;

                if (rights != null)
                {
                    read = rights.equals(MBeanInvocationHandlerImpl.READONLY)
                           || rights.equals(MBeanInvocationHandlerImpl.READWRITE);
                    write = rights.equals(MBeanInvocationHandlerImpl.READWRITE);
                    admin = rights.equals(MBeanInvocationHandlerImpl.ADMIN);
                }

                Object[] itemData = {user.getName(), read, write, admin};
                CompositeData messageData = new CompositeDataSupport(_userDataType, _userItemNames, itemData);
                userList.put(messageData);
            }
        }
        catch (OpenDataException e)
        {
            _logger.warn("Unable to create user list due to :" + e);
            return null;
        }

        return userList;
    }

    /*** Broker Methods **/

    /**
     * setPrincipalDatabase
     *
     * @param database set The Database to use for user lookup
     */
    public void setPrincipalDatabase(PrincipalDatabase database)
    {
        _principalDatabase = database;
    }

    /**
     * setAccessFile
     *
     * @param accessFile the file to use for updating.
     *
     * @throws java.io.IOException If the file cannot be accessed
     * @throws org.apache.commons.configuration.ConfigurationException
     *                             if checks on the file fail.
     */
    public void setAccessFile(String accessFile) throws IOException, ConfigurationException
    {
        _accessFileName = accessFile;

        if (_accessFileName != null)
        {
            loadAccessFile();
        }
        else
        {
            _logger.warn("Access rights file specified is null. Access rights not changed.");
        }
    }

    private void loadAccessFile() throws IOException, ConfigurationException
    {
        try
        {
            _accessRightsUpdate.lock();

            Properties accessRights = new Properties();

            File accessFile = new File(_accessFileName);

            if (!accessFile.exists())
            {
                throw new ConfigurationException("'" + _accessFileName + "' does not exist");
            }

            if (!accessFile.canRead())
            {
                throw new ConfigurationException("Cannot read '" + _accessFileName + "'.");
            }

            if (!accessFile.canWrite())
            {
                _logger.warn("Unable to write to access file '" + _accessFileName + "' changes will not be preserved.");
            }

            accessRights.load(new FileInputStream(accessFile));
            checkAccessRights(accessRights);
            setAccessRights(accessRights);
        }
        finally
        {
            if (_accessRightsUpdate.isHeldByCurrentThread())
            {
                _accessRightsUpdate.unlock();
            }
        }
    }

    private void checkAccessRights(Properties accessRights)
    {
        Enumeration values = accessRights.propertyNames();

        while (values.hasMoreElements())
        {
            String user = (String) values.nextElement();

            if (_principalDatabase.getUser(user) == null)
            {
                _logger.warn("Access rights contains user '" + user + "' but there is no authentication data for that user");
            }
        }
    }

    private void saveAccessFile()
    {
        try
        {
            _accessRightsUpdate.lock();
            try
            {
                // remove old temporary file
                File tmp = new File(_accessFileName + ".tmp");
                if (tmp.exists())
                {
                    tmp.delete();
                }

                //remove old backup
                File old = new File(_accessFileName + ".old");
                if (old.exists())
                {
                    old.delete();
                }

                // Rename current file
                File rights = new File(_accessFileName);
                rights.renameTo(old);

                FileOutputStream output = new FileOutputStream(tmp);
                _accessRights.store(output, "Generated by AMQUserManagementMBean Console : Last edited by user:" + getCurrentJMXUser());
                output.close();

                // Rename new file to main file
                tmp.renameTo(rights);

                // delete tmp
                tmp.delete();
            }
            catch (IOException e)
            {
                _logger.warn("Problem occured saving '" + _accessFileName + "' changes may not be preserved. :" + e);
            }
        }
        finally
        {
            if (_accessRightsUpdate.isHeldByCurrentThread())
            {
                _accessRightsUpdate.unlock();
            }
        }
    }

    private String getCurrentJMXUser()
    {
        AccessControlContext acc = AccessController.getContext();
        Subject subject = Subject.getSubject(acc);

        // Retrieve JMXPrincipal from Subject
        Set<JMXPrincipal> principals = subject.getPrincipals(JMXPrincipal.class);
        if (principals == null || principals.isEmpty())
        {
            return "Unknown user principals were null";
        }

        Principal principal = principals.iterator().next();
        return principal.getName();
    }

    /**
     * user=read user=write user=readwrite user=admin
     *
     * @param accessRights The properties list of access rights to process
     */
    private void setAccessRights(Properties accessRights)
    {
        _logger.debug("Setting Access Rights:" + accessRights);
        _accessRights = accessRights;
        MBeanInvocationHandlerImpl.setAccessRights(_accessRights);
    }
}
