/**
 * Copyright (c) 2009--2014 Red Hat, Inc.
 *
 * This software is licensed to you under the GNU General Public License,
 * version 2 (GPLv2). There is NO WARRANTY for this software, express or
 * implied, including the implied warranties of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
 * along with this software; if not, see
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
 *
 * Red Hat trademarks are not licensed under GPLv2. No permission is
 * granted to use or replicate Red Hat trademarks that are incorporated
 * in this software or its documentation.
 */
package com.redhat.rhn.frontend.xmlrpc.user;

import com.redhat.rhn.FaultException;
import com.redhat.rhn.common.conf.UserDefaults;
import com.redhat.rhn.common.hibernate.LookupException;
import com.redhat.rhn.common.localization.LocalizationService;
import com.redhat.rhn.common.security.PermissionException;
import com.redhat.rhn.common.util.MethodUtil;
import com.redhat.rhn.common.util.StringUtil;
import com.redhat.rhn.common.validator.ValidatorError;
import com.redhat.rhn.domain.role.Role;
import com.redhat.rhn.domain.role.RoleFactory;
import com.redhat.rhn.domain.server.ManagedServerGroup;
import com.redhat.rhn.domain.server.ServerGroup;
import com.redhat.rhn.domain.server.ServerGroupFactory;
import com.redhat.rhn.domain.user.User;
import com.redhat.rhn.domain.user.UserFactory;
import com.redhat.rhn.frontend.action.common.BadParameterException;
import com.redhat.rhn.frontend.xmlrpc.BaseHandler;
import com.redhat.rhn.frontend.xmlrpc.DeleteUserException;
import com.redhat.rhn.frontend.xmlrpc.InvalidOperationException;
import com.redhat.rhn.frontend.xmlrpc.InvalidServerGroupException;
import com.redhat.rhn.frontend.xmlrpc.LookupServerGroupException;
import com.redhat.rhn.frontend.xmlrpc.NoSuchRoleException;
import com.redhat.rhn.frontend.xmlrpc.PermissionCheckFailureException;
import com.redhat.rhn.frontend.xmlrpc.UserNeverLoggedInException;
import com.redhat.rhn.frontend.xmlrpc.UserNotUpdatedException;
import com.redhat.rhn.manager.SatManager;
import com.redhat.rhn.manager.system.ServerGroupManager;
import com.redhat.rhn.manager.user.CreateUserCommand;
import com.redhat.rhn.manager.user.DeleteSatAdminException;
import com.redhat.rhn.manager.user.UpdateUserCommand;
import com.redhat.rhn.manager.user.UserManager;

import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * UserHandler
 * Corresponds to User.pm in old perl code.
 * @version $Rev$
 * @xmlrpc.namespace user
 * @xmlrpc.doc User namespace contains methods to access common user functions
 * available from the web user interface.
 */
public class UserHandler extends BaseHandler {

    /**
     * Contains a mapping of details key as submitted by the call to the
     * {@link #setDetails(String, String, Map)} to the internal key used in the command
     * and domain objects. This is a band-aid to make the external API read correctly
     * (first_name instead of first_names) without having to refactor the entire code
     * base to use the singular version (for instance, User still uses first_names and
     * will be a significant change to refactor that). For more information, see
     * bugzilla 469957.
     */
    private static final Map<String, String> USER_EDITABLE_DETAILS =
        new HashMap<String, String>();
    static {
        USER_EDITABLE_DETAILS.put("first_name", "first_names");
        USER_EDITABLE_DETAILS.put("first_names", "first_names");
        USER_EDITABLE_DETAILS.put("last_name", "last_name");
        USER_EDITABLE_DETAILS.put("email", "email");
        USER_EDITABLE_DETAILS.put("prefix", "prefix");
        USER_EDITABLE_DETAILS.put("password", "password");
    }

    /**
     * Lists the users in the org.
     * @param loggedInUser The current user
     * @return Returns a list of userids and logins
     * @throws FaultException A FaultException is thrown if the loggedInUser
     * doesn't have permissions to list the users in their org.
     *
     * @xmlrpc.doc Returns a list of users in your organization.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.returntype
     * #array()
     *     $UserSerializer
     * #array_end()
     */
    public List listUsers(User loggedInUser) throws FaultException {
        // Get the logged in user
        try {
            List users = UserManager.usersInOrg(loggedInUser);
            return users;
        }
        catch (PermissionException e) {
            throw new PermissionCheckFailureException();
        }
    }

    /**
     * Lists the roles for a user
     * @param loggedInUser The current user
     * @param login The login for the user you want to get the roles for
     * @return Returns a list of roles for the user specified by login
     * @throws FaultException A FaultException is thrown if the user doesn't have access
     * to lookup the user corresponding to login or if the user does not exist.
     *
     * @xmlrpc.doc Returns a list of the user's roles.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.returntype #array_single("string", "(role label)")
     */
    public Object[] listRoles(User loggedInUser, String login) throws FaultException {
        // Get the logged in user
        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(loggedInUser, login);
        List roles = new ArrayList(); //List of role labels to return

        //Loop through the target users roles and stick the labels into the ArrayList
        Set roleObjects = target.getPermanentRoles();
        for (Iterator itr = roleObjects.iterator(); itr.hasNext();) {
            Role r = (Role) itr.next();
            roles.add(r.getLabel());
        }

        return roles.toArray();
    }

    /**
     * Lists all the roles that can be assign by this user.
     * @param loggedInUser The current user
     * @return Returns a list of assignable roles for user
     * @throws FaultException A FaultException is thrown if the logged doesn't have access.
     *
     * @xmlrpc.doc Returns a list of user roles that this user can assign to others.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.returntype #array_single("string", "(role label)")
     */
    public Set<String> listAssignableRoles(User loggedInUser) {
        return getAssignableRoles(loggedInUser);
    }

    /**
     * Gets details for a given user. These details include first names, last name, email,
     * prefix, last login date, and created on date.
     * @param loggedInUser The current user
     * @param login The login for the user you want the details for
     * @return Returns a Map containing the details for the given user.
     * @throws FaultException A FaultException is thrown if the user doesn't have access
     * to lookup the user corresponding to login or if the user does not exist.
     *
     * @xmlrpc.doc Returns the details about a given user.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.returntype
     *   #struct("user details")
     *     #prop_desc("string", "first_names", "deprecated, use first_name")
     *     #prop("string", "first_name")
     *     #prop("string", "last_name")
     *     #prop("string", "email")
     *     #prop("int", "org_id")
     *     #prop("string", "org_name")
     *     #prop("string", "prefix")
     *     #prop("string", "last_login_date")
     *     #prop("string", "created_date")
     *     #prop_desc("boolean", "enabled", "true if user is enabled,
     *     false if the user is disabled")
     *     #prop_desc("boolean", "use_pam", "true if user is configured to use
     *     PAM authentication")
     *     #prop_desc("boolean", "read_only", "true if user is readonly")
     *     #prop_desc("boolean", "errata_notification", "true if errata e-mail notification
     *     is enabled for the user")
     *   #struct_end()
     */
    public Map getDetails(User loggedInUser, String login) throws FaultException {
        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(loggedInUser, login);
        LocalizationService ls = LocalizationService.getInstance();

        Map ret = new HashMap();
        ret.put("first_names", StringUtils.defaultString(target.getFirstNames()));
        ret.put("first_name", StringUtils.defaultString(target.getFirstNames()));
        ret.put("last_name",   StringUtils.defaultString(target.getLastName()));
        ret.put("email",       StringUtils.defaultString(target.getEmail()));
        ret.put("prefix",      StringUtils.defaultString(target.getPrefix()));

        //Last login date
        String lastLoggedIn = target.getLastLoggedIn() == null ?
                                  "" : ls.formatDate(target.getLastLoggedIn());
        ret.put("last_login_date", lastLoggedIn);

        //Created date
        String created = target.getCreated() == null ?
                                  "" : ls.formatDate(target.getCreated());
        ret.put("created_date", created);
        ret.put("org_id", loggedInUser.getOrg().getId());
        ret.put("org_name", loggedInUser.getOrg().getName());

        if (target.isDisabled()) {
            ret.put("enabled", Boolean.FALSE);
        }
        else {
            ret.put("enabled", Boolean.TRUE);
        }
        ret.put("use_pam", target.getUsePamAuthentication());
        ret.put("read_only", target.isReadOnly());
        ret.put("errata_notification", target.getEmailNotify() == 1);

        return ret;
    }

    /**
     * Sets the details for a given user. Settable details include: first names,
     * last name, email, prefix, and password.
     * @param loggedInUser The current user
     * user.
     * @param login The login for the user you want to edit
     * @param details A map containing the new details values
     * @return Returns 1 if edit was successful, an error is thrown otherwise
     * @throws FaultException A FaultException is thrown if the user doesn't
     * have access to lookup the user corresponding to login or if the user
     * does not exist.
     *
     * @xmlrpc.doc Updates the details of a user.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.param
     *   #struct("user details")
     *     #prop_desc("string", "first_names", "deprecated, use first_name")
     *     #prop("string", "first_name")
     *     #prop("string", "last_name")
     *     #prop("string", "email")
     *     #prop("string", "prefix")
     *     #prop("string", "password")
     *   #struct_end()
     * @xmlrpc.returntype #return_int_success()
     */
    public int setDetails(User loggedInUser, String login, Map details)
        throws FaultException {

        validateMap(USER_EDITABLE_DETAILS.keySet(), details);

        // Lookup user handles the logic for making sure that the loggedInUser
        // has access to the login they are trying to edit.
        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(
                loggedInUser, login);

        UpdateUserCommand uuc = new UpdateUserCommand(target);

        // Process each entry passed in by the user
        for (Object userKey : details.keySet()) {

            // Check to make sure we have an internal key mapping to prevent issues
            // if the user passes in cruft
            String internalKey = USER_EDITABLE_DETAILS.get(userKey);
            if (internalKey != null) {
                String newValue = StringUtils.defaultString((String) details.get(userKey));
                prepareAttributeUpdate(internalKey, uuc, newValue);
            }
        }

        try {
            uuc.updateUser();
        }
        catch (IllegalArgumentException iae) {
            throw new UserNotUpdatedException(iae.getMessage());
        }

        // If we made it here without an exception, then we are a.o.k.
        return 1;
    }

    /**
     * Handles the vagaries related to granting or revoking sat admin role
     * @param loggedInUser the logged in user
     * @param login the login of the user who needs to be granted/revoked sat admin role
     * @param grant true if granting the role to the login, false for revoking...
     * @return 1 if it success.. Ofcourse error on failure..
     */
    private int  modifySatAdminRole(User loggedInUser, String login, boolean grant) {
        ensureUserRole(loggedInUser, RoleFactory.SAT_ADMIN);
        SatManager manager = SatManager.getInstance();
        User user = UserFactory.lookupByLogin(login);
        if (grant) {
            manager.grantSatAdminRoleTo(user, loggedInUser);
        }
        else {
            manager.revokeSatAdminRoleFrom(user, loggedInUser);
        }
        UserManager.storeUser(user);
        return 1;
    }

    /**
     * Returns all roles that are assignable to a given user
     * @return all the role labels that are assignable to a user.
     */
    private Set<String> getAssignableRoles(User user) {
        Set <String> assignableRoles = new LinkedHashSet<String>();
        for (Role r : UserManager.listRolesAssignableBy(user)) {
            assignableRoles.add(r.getLabel());
        }
        return assignableRoles;
    }

    /**
     * Validates that the select roles is among the ones we support.
     * @param role the role that user wanted to be assigned
     * @param user the logged in user who wants to assign the given role.
     */
    private void validateRoleInputs(String role, User user) {
        Set <String> assignableRoles = getAssignableRoles(user);
        if (!assignableRoles.contains(role)) {
            String msg = "Role with the label [%s] cannot be " +
                          "assigned/revoked from the user." +
                         " Possible Roles assignable/revokable by this user %s";

            throw new NoSuchRoleException(String.format(msg, role,
                                                    assignableRoles.toString()));
        }
    }

    /**
     * Adds a role to the given user
     * @param loggedInUser The current user
     * @param login The login for the user you would like to add the role to
     * @param role The role you would like to give the user
     * @return Returns 1 if successful (exception otherwise)
     * @throws FaultException A FaultException is thrown if the user doesn't have access
     * to lookup the user corresponding to login or if the user does not exist.
     *
     * @xmlrpc.doc Adds a role to a user.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User login name to update.")
     * @xmlrpc.param #param_desc("string", "role", "Role label to add.  Can be any of:
     * satellite_admin, org_admin, channel_admin, config_admin, system_group_admin,
     * activation_key_admin, or monitoring_admin.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int addRole(User loggedInUser, String login, String role) throws FaultException {
        validateRoleInputs(role, loggedInUser);
        if (RoleFactory.SAT_ADMIN.getLabel().equals(role)) {
            return modifySatAdminRole(loggedInUser, login, true);
        }
        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(loggedInUser, login);
        // Retrieve the role object corresponding to the role label passed in and
        // add to user
        Role r = RoleFactory.lookupByLabel(role);
        target.addPermanentRole(r);
        UserManager.storeUser(target);
        return 1;
    }


    /**
     * Removes a role from the given user
     * @param loggedInUser The current user
     * @param login The login for the user you would like to remove the role from
     * @param role The role you would like to remove from the user
     * @return Returns 1 if successful (exception otherwise)
     * @throws FaultException A FaultException is thrown if the user doesn't have access
     * to lookup the user corresponding to login or if the user does not exist.
     *
     * @xmlrpc.doc Remove a role from a user.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User login name to update.")
     * @xmlrpc.param #param_desc("string", "role", "Role label to remove.  Can be any of:
     * satellite_admin, org_admin, channel_admin, config_admin, system_group_admin,
     * activation_key_admin, or monitoring_admin.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int removeRole(User loggedInUser, String login, String role)
        throws FaultException {
        validateRoleInputs(role, loggedInUser);

        if (RoleFactory.SAT_ADMIN.getLabel().equals(role)) {
            return modifySatAdminRole(loggedInUser, login, false);
        }

        ensureOrgAdmin(loggedInUser);
        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(loggedInUser, login);

        /*
         * Perform some error checking here... we need to make sure that this
         * isn't the last org_admin in the org trying to remove org_admin
         * status from himself.
         */
        if (!target.isReadOnly()) {
            if (role.equals(RoleFactory.ORG_ADMIN.getLabel()) &&
                target.hasRole(RoleFactory.ORG_ADMIN) &&
                target.getOrg().numActiveOrgAdmins() <= 1) {
                    throw new PermissionCheckFailureException();
            }
        }

        // Retrieve the role object corresponding to the role label passed in and
        // remove from user
        Role r = RoleFactory.lookupByLabel(role);
        target.removePermanentRole(r);

        UserManager.storeUser(target);
        return 1;
    }

    /**
     * Creates a new user
     * @param loggedInUser The current user
     * @param desiredLogin The login for the new user
     * @param desiredPassword The password for the new user
     * @param firstName The first name of the new user
     * @param lastName The last name of the new user
     * @param email The email address for the new user
     * @return Returns 1 if successful (exception otherwise)
     * @throws FaultException A FaultException is thrown if the loggedInUser doesn't have
     * permissions to create new users in thier org.
     *
     * @xmlrpc.doc Create a new user.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "desiredLogin", "Desired login name, will fail if
     * already in use.")
     * @xmlrpc.param #param("string", "desiredPassword")
     * @xmlrpc.param #param("string", "firstName")
     * @xmlrpc.param #param("string", "lastName")
     * @xmlrpc.param #param_desc("string", "email", "User's e-mail address.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int create(User loggedInUser, String desiredLogin, String desiredPassword,
                   String firstName, String lastName, String email) throws FaultException {

        // If we didn't get a value for pamAuth, default to no
        return create(loggedInUser, desiredLogin, desiredPassword, firstName, lastName,
                      email, new Integer(0));
    }

    /**
     * Creates a new user
     * @param loggedInUser The current user
     * @param desiredLogin The login for the new user
     * @param desiredPassword The password for the new user
     * @param firstName The first name of the new user
     * @param lastName The last name of the new user
     * @param email The email address for the new user
     * @param usePamAuth Should this user authenticate via PAM?
     * @return Returns 1 if successful (exception otherwise)
     * @throws FaultException A FaultException is thrown if the loggedInUser doesn't have
     * permissions to create new users in thier org.
     *
     * @xmlrpc.doc Create a new user.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "desiredLogin", "Desired login name,
     * will fail if already in use.")
     * @xmlrpc.param #param("string", "desiredPassword")
     * @xmlrpc.param #param("string", "firstName")
     * @xmlrpc.param #param("string", "lastName")
     * @xmlrpc.param #param_desc("string", "email", "User's e-mail address.")
     * @xmlrpc.param #param_desc("int", "usePamAuth", "1 if you wish to use PAM
     * authentication for this user, 0 otherwise.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int create(User loggedInUser, String desiredLogin, String desiredPassword,
                      String firstName, String lastName, String email, Integer usePamAuth)
                      throws FaultException {
        //Logged in user must be an org admin and we must be on a sat to do this.
        ensureOrgAdmin(loggedInUser);
        ensurePasswordOrPamAuth(usePamAuth, desiredPassword);

        boolean pamAuth = BooleanUtils.toBoolean(
                usePamAuth, new Integer(1), new Integer(0));

        if (pamAuth) {
            desiredPassword = getDefaultPasswordForPamAuth();
        }

        CreateUserCommand command = new CreateUserCommand();
        command.setUsePamAuthentication(pamAuth);
        command.setLogin(desiredLogin);
        command.setPassword(desiredPassword);
        command.setFirstNames(firstName);
        command.setLastName(lastName);
        command.setEmail(email);
        command.setOrg(loggedInUser.getOrg());
        command.setCompany(loggedInUser.getCompany());

        //Validate the user to be
        ValidatorError[] errors = command.validate();
        if (errors.length > 0) {
            StringBuilder errorString = new StringBuilder();
            LocalizationService ls = LocalizationService.getInstance();
            //Build a sane error message here
            for (int i = 0; i < errors.length; i++) {
                ValidatorError err = errors[i];
                errorString.append(ls.getMessage(err.getKey(), err.getValues()));
                if (i != errors.length - 1) {
                    errorString.append(" :: ");
                }
            }
            //Throw a BadParameterException with our message string
            throw new BadParameterException(errorString.toString());
        }

        command.storeNewUser();
        return 1;
    }

    /**
     * Deletes a user
     * @param loggedInUser The current user
     * @param login The login for the user you would like to delete
     * @return Returns 1 if successful (exception otherwise)
     * @throws FaultException A FaultException is thrown if the user doesn't have access
     * to lookup the user corresponding to login or if the user does not exist.
     *
     * @xmlrpc.doc Delete a user.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User login name to delete.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int delete(User loggedInUser, String login) throws FaultException {
        ensureOrgAdmin(loggedInUser);
        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(loggedInUser, login);

        try {
            UserManager.deleteUser(loggedInUser, target.getId());
        }
        catch (DeleteSatAdminException e) {
            throw new DeleteUserException("user.cannot.delete.last.sat.admin");
        }

        return 1;
    }

    /**
     * Disable a user
     * @param loggedInUser The current user
     * @param login The login for the user you would like to disable
     * @return Returns 1 if successful (exception otherwise)
     * @throws FaultException A FaultException is thrown if the user doesn't have access
     * to lookup the user corresponding to login or if the user does not exist.
     *
     * @xmlrpc.doc Disable a user.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User login name to disable.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int disable(User loggedInUser, String login) throws FaultException {
        ensureOrgAdmin(loggedInUser);

        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(loggedInUser, login);
        UserManager.disableUser(loggedInUser, target);

        return 1;
    }

    /**
     * Enable a user
     * @param loggedInUser The current user
     * @param login The login for the user you would like to enable
     * @return Returns 1 if successful (exception otherwise)
     * @throws FaultException A FaultException is thrown if the user doesn't have access
     * to lookup the user corresponding to login or if the user does not exist.
     *
     * @xmlrpc.doc Enable a user.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User login name to enable.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int enable(User loggedInUser, String login) throws FaultException {
        ensureOrgAdmin(loggedInUser);

        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(loggedInUser, login);
        UserManager.enableUser(loggedInUser, target);

        return 1;
    }

    /**
     * Toggles whether or not a user users pamAuthentication or the basic RHN db auth.
     * @param loggedInUser The current user
     * @param login The login for the user you would like to change
     * @param val The value you would like to set this to (1 = true, 0 = false)
     * @return Returns 1 if successful (exception otherwise)
     * @throws FaultException A FaultException is thrown if the user doesn't have access
     * to lookup the user corresponding to login or if the user does not exist.
     *
     * @xmlrpc.doc Toggles whether or not a user uses PAM authentication or
     * basic RHN authentication.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.param #param("int", "pam_value")
     *   #options()
     *     #item("1 to enable PAM authentication")
     *     #item("0 to disable.")
     *   #options_end()
     * @xmlrpc.returntype #return_int_success()
     */
    public int usePamAuthentication(User loggedInUser, String login, Integer val)
        throws FaultException {
        // Only org admins can use this method.
        ensureOrgAdmin(loggedInUser);
        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(loggedInUser, login);

        if (val.equals(new Integer(1))) {
            target.setUsePamAuthentication(true);
        }
        else {
            target.setUsePamAuthentication(false);
        }

        UserManager.storeUser(target);

        return 1;
    }


    private void ensurePasswordOrPamAuth(Integer usePamAuth, String password)
        throws FaultException {
        if (!BooleanUtils.toBoolean(usePamAuth, new Integer(1), new Integer(0)) &&
                StringUtils.isEmpty(password)) {
            throw new FaultException(-501, "passwordRequiredOrUsePam",
                    "Password is required if not using PAM authentication");
        }
    }

    private String getDefaultPasswordForPamAuth() {
        // taken from line 169 of CreateUserAction
        // this is utter crap.  We don't require a password when
        // we set use pam authentication, yet the password field
        // in the database is NOT NULL.  So we have to create this
        // stupid HACK!  Actually this is beyond HACK.
        return RandomStringUtils.random(UserDefaults.get().getMinPasswordLength());

    }

    private void prepareAttributeUpdate(String attrName, UpdateUserCommand cmd,
            String value) {
        String methodName = StringUtil.beanify("set_" + attrName);
        Object[] params = {value};
        MethodUtil.callMethod(cmd, methodName, params);
    }

    /**
     * Add ServerGroup to the list of Default System groups. The ServerGroup
     * <strong>MUST</strong> exist otherwise a IllegalArgumentException is
     * thrown.
     * @param loggedInUser The current user
     * in user.
     * @param login The login for the user whose Default ServerGroup list will
     * be affected.
     * @param name name of ServerGroup.
     * @return Returns 1 if successful (exception otherwise)
     *
     * @xmlrpc.doc Add system group to user's list of default system groups.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.param #param("string", "serverGroupName")
     * @xmlrpc.returntype #return_int_success()
     */
    public int addDefaultSystemGroup(User loggedInUser, String login, String name) {
        List<String> ids = new LinkedList<String>();
        ids.add(name);
        return addDefaultSystemGroups(loggedInUser, login, ids);
    }

    /**
     * Add ServerGroups to the list of Default System groups. The ServerGroups
     * <strong>MUST</strong> exist otherwise a IllegalArgumentException is
     * thrown.
     * @param loggedInUser The current user
     * in user.
     * @param login The login for the user whose Default ServerGroup list will
     * be affected.
     * @param sgNames names of ServerGroups.
     * @return Returns 1 if successful (exception otherwise)
     *
     * @xmlrpc.doc Add system groups to user's list of default system groups.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.param #array_single("string", "serverGroupName")
     * @xmlrpc.returntype #return_int_success()
     */
    public int addDefaultSystemGroups(User loggedInUser, String login, List sgNames) {

        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(
                loggedInUser, login);

        if (sgNames == null || sgNames.size() < 1) {
            throw new IllegalArgumentException("no servergroup names supplied");
        }

        List groups = ServerGroupFactory.listManagedGroups(target.getOrg());

        Map groupMap = new HashMap();

        // sigh.  After looking through all of the apache collections package
        // I couldn't find anything that would create a map from a list using
        // a property from the object in the list as the key. This is where
        // python would be useful.
        for (Iterator itr = groups.iterator(); itr.hasNext();) {
            ServerGroup sg = (ServerGroup) itr.next();
            groupMap.put(sg.getName(), sg);
        }

        // Doing full check of all supplied names, if one is bad
        // throw an exception, prior to altering the DefaultSystemGroup Set.
        for (Iterator itr = sgNames.iterator(); itr.hasNext();) {
            String name = (String)itr.next();
            ServerGroup sg = (ServerGroup) groupMap.get(name);
            if (sg == null) {
                throw new LookupServerGroupException(name);
            }
        }

        // now for the real reason we're in this method.
        Set defaults = target.getDefaultSystemGroupIds();
        for (Iterator itr = sgNames.iterator(); itr.hasNext();) {
            ServerGroup sg = (ServerGroup) groupMap.get(itr.next());
            if (sg != null) {
                // not a simple add to the groups.  Needs to call
                // UserManager as DataSource is being used.
                defaults.add(sg.getId());
            }
        }

        UserManager.setDefaultSystemGroupIds(target, defaults);
        UserManager.storeUser(target);

        return 1;
    }

    /**
     * Remove ServerGroup from the list of Default System groups. The
     * ServerGroup <strong>MUST</strong> exist otherwise a
     * IllegalArgumentException is thrown.
     * @param loggedInUser The current user
     * in user.
     * @param login The login for the user whose Default ServerGroup list will
     * be affected.
     * @param sgName Name of ServerGroup.
     * @return Returns 1 if successful (exception otherwise)
     *
     * @xmlrpc.doc Remove a system group from user's list of default system groups.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.param #param("string", "serverGroupName")
     * @xmlrpc.returntype #return_int_success()
     */
    public int removeDefaultSystemGroup(User loggedInUser, String login, String sgName) {
        List<String> names = new LinkedList<String>();
        names.add(sgName);
        return removeDefaultSystemGroups(loggedInUser, login, names);
    }

    /**
     * Remove ServerGroups from the list of Default System groups. The
     * ServerGroups <strong>MUST</strong> exist otherwise a
     * IllegalArgumentException is thrown.
     * @param loggedInUser The current user
     * in user.
     * @param login The login for the user whose Default ServerGroup list will
     * be affected.
     * @param sgNames Names of ServerGroups.
     * @return Returns 1 if successful (exception otherwise)
     *
     * @xmlrpc.doc Remove system groups from a user's list of default system groups.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.param #array_single("string", "serverGroupName")
     * @xmlrpc.returntype #return_int_success()
     */
    public int removeDefaultSystemGroups(User loggedInUser, String login, List sgNames) {

        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(
                loggedInUser, login);

        if (sgNames == null || sgNames.size() < 1) {
            throw new IllegalArgumentException("no servergroup names supplied");
        }

        List groups = ServerGroupFactory.listManagedGroups(target.getOrg());
        Map groupMap = new HashMap();

        // sigh.  After looking through all of the apache collections package
        // I couldn't find anything that would create a map from a list using
        // a property from the object in the list as the key. This is where
        // python would be useful.
        for (Iterator itr = groups.iterator(); itr.hasNext();) {
            ServerGroup sg = (ServerGroup) itr.next();
            groupMap.put(sg.getName(), sg);
        }

        // Doing full check of all supplied names, if one is bad
        // throw an exception, prior to altering the DefaultSystemGroup Set.
        for (Iterator itr = sgNames.iterator(); itr.hasNext();) {
            String name = (String)itr.next();
            ServerGroup sg = (ServerGroup) groupMap.get(name);
            if (sg == null) {
                throw new LookupServerGroupException(name);
            }
        }

        // now for the real reason we're in this method.
        Set defaults = target.getDefaultSystemGroupIds();
        for (Iterator itr = sgNames.iterator(); itr.hasNext();) {
            ServerGroup sg = (ServerGroup) groupMap.get(itr.next());
            if (sg != null) {
                // not a simple remove to the groups.  Needs to call
                // UserManager as DataSource is being used.
                defaults.remove(sg.getId());
            }
        }

        UserManager.setDefaultSystemGroupIds(target, defaults);
        UserManager.storeUser(target);

        return 1;
    }

    /**
     * Returns default system groups for the given login.
     * @param loggedInUser The current user
     * in user.
     * @param login The login for the user whose Default ServerGroup list is
     * sought.
     * @return default system groups for the given login
     *
     * @xmlrpc.doc Returns a user's list of default system groups.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.returntype
     *   #array()
     *     #struct("system group")
     *       #prop("int", "id")
     *       #prop("string", "name")
     *       #prop("string", "description")
     *       #prop("int", "system_count")
     *       #prop_desc("int", "org_id", "Organization ID for this system group.")
     *     #struct_end()
     *   #array_end()
     */
    public Object[] listDefaultSystemGroups(User loggedInUser, String login) {
        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(
                loggedInUser, login);
        Set<Long> ids =  target.getDefaultSystemGroupIds();


        List <ServerGroup> sgs = new ArrayList(ids.size());
        for (Long id : ids) {
            sgs.add(ServerGroupFactory.lookupByIdAndOrg(id, target.getOrg()));
        }
        return sgs.toArray();
    }

    /**
     * Returns the ServerGroups that the user can administer.
     * @param loggedInUser The current user
     * in user.
     * @param login The login for the user whose ServerGroups are sought.
     * @return the ServerGroups that the user can administer.
     * @throws FaultException A FaultException is thrown if the user doesn't
     * have access to lookup the user corresponding to login or if the user
     * does not exist.
     *
     * @xmlrpc.doc Returns the system groups that a user can administer.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.returntype
     *   #array()
     *     #struct("system group")
     *       #prop("int", "id")
     *       #prop("string", "name")
     *       #prop("string", "description")
     *       #prop("int", "system_count")
     *       #prop_desc("int", "org_id", "Organization ID for this system group.")
     *     #struct_end()
     *   #array_end()
     */
    public Object[] listAssignedSystemGroups(User loggedInUser, String login)
        throws FaultException {
        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(
                    loggedInUser, login);
        List groups = ServerGroupFactory.listAdministeredServerGroups(target);
        return groups.toArray();
    }

    /**
     * Returns the last logged in time of the given user.
     * @param loggedInUser The current user
     * in user.
     * @param login The login of the user.
     * @return last logged in time
     * @throws UserNeverLoggedInException if the given user has never logged in.
     *
     * @xmlrpc.doc Returns the time user last logged in.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.returntype dateTime.iso8601
     */
    public Date getLoggedInTime(User loggedInUser, String login)
        throws UserNeverLoggedInException {

        User target = XmlRpcUserHelper.getInstance().lookupTargetUser(
                    loggedInUser, login);
        Date d = target.getLastLoggedIn();
        if (d != null) {
            return d;
        }
        throw new UserNeverLoggedInException();
    }



    /**
     * remove system group association from a user
     * @param loggedInUser The current user
     * @param login the user's login that we want to remove the association from
     * @param systemGroupNames list of system group names to remove
     * @param setDefault if true the default group will be removed from the users's
     *      group defaults
     * @return 1 on success
     *
     * @xmlrpc.doc Remove system groups from a user's list of assigned system groups.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.param #array_single("string", "serverGroupName")
     * @xmlrpc.param #param_desc("boolean", "setDefault", "Should system groups also be
     * removed from the user's list of default system groups.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int removeAssignedSystemGroups(User loggedInUser,
            String login, List<String> systemGroupNames, Boolean setDefault) {
        ensureUserRole(loggedInUser, RoleFactory.ORG_ADMIN);

        if (setDefault) {
            removeDefaultSystemGroups(loggedInUser, login, systemGroupNames);
         }

        User user = UserManager.lookupUser(loggedInUser, login);
        ServerGroupManager manager = ServerGroupManager.getInstance();

        // Iterate once to lookup the server groups and avoid removing some when
        // an exception will only be thrown later:
        List<ManagedServerGroup> groups = new LinkedList<ManagedServerGroup>();
        for (String name : systemGroupNames) {
            ManagedServerGroup sg = null;
            try {
                sg = manager.lookup(name, user);
            }
            catch (LookupException e) {
                throw new InvalidServerGroupException();
            }
            groups.add(sg);
        }

        for (ManagedServerGroup sg : groups) {
            UserManager.revokeServerGroupPermission(user, sg.getId().longValue());
        }

        return 1;
    }

    /**
     * remove system group association from a user
     * @param loggedInUser The current user
     * @param login the user's login that we want to remove the association from
     * @param systemGroupName  system group name to remove
     * @param setDefault if true the default group will be removed from the users's
     *      group defaults
     * @return 1 on success
     *
     * @xmlrpc.doc Remove system group from the user's list of assigned system groups.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.param #param("string", "serverGroupName")
     * @xmlrpc.param #param_desc("boolean", "setDefault", "Should system group also
     * be removed from the user's list of default system groups.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int removeAssignedSystemGroup(User loggedInUser,
            String login, String systemGroupName, Boolean setDefault) {
            List groups = new ArrayList();
            groups.add(systemGroupName);
            return removeAssignedSystemGroups(loggedInUser, login, groups, setDefault);
    }



    /**
     * Add to the user's list of assigned system groups.
     *
     * @param loggedInUser The current user
     * @param login User to modify.
     * @param sgName Server group Name.
     * @param setDefault True to also add group to the user's default system groups.
     * @return Returns 1 if successful (exception thrown otherwise)
     *
     * @xmlrpc.doc Add system group to user's list of assigned system groups.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.param #param("string", "serverGroupName")
     * @xmlrpc.param #param_desc("boolean", "setDefault", "Should system group also be
     * added to user's list of default system groups.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int addAssignedSystemGroup(User loggedInUser, String login, String sgName,
            Boolean setDefault) {
        List<String> names = new LinkedList<String>();
        names.add(sgName);
        return addAssignedSystemGroups(loggedInUser, login, names, setDefault);
    }

    /**
     * Add to the user's list of assigned system groups.
     *
     * @param loggedInUser The current user
     * @param login User to modify.
     * @param sgNames List of server group Names.
     * @param setDefault True to also add groups to the user's default system groups.
     * @return Returns 1 if successful (exception thrown otherwise)
     *
     * @xmlrpc.doc Add system groups to user's list of assigned system groups.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.param #array_single("string", "serverGroupName")
     * @xmlrpc.param #param_desc("boolean", "setDefault", "Should system groups also be
     * added to user's list of default system groups.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int addAssignedSystemGroups(User loggedInUser, String login, List sgNames,
            Boolean setDefault) {

        User targetUser = XmlRpcUserHelper.getInstance().lookupTargetUser(
                loggedInUser, login);

        if (sgNames == null || sgNames.size() < 1) {
            throw new IllegalArgumentException("no servergroup names supplied");
        }


        // Iterate once just to make sure all the server groups exist. Done to
        // prevent adding a bunch of valid groups and then throwing an exception
        // when coming across one that doesn't exist.
        List<ManagedServerGroup> groups = new LinkedList<ManagedServerGroup>();
        for (Iterator it = sgNames.iterator(); it.hasNext();) {
            String serverGroupName =  (String)it.next();

            // Make sure the server group exists:
            ServerGroupManager manager = ServerGroupManager.getInstance();
            ManagedServerGroup group;
            try {
                group = manager.lookup(serverGroupName, loggedInUser);
            }
            catch (LookupException e) {
                throw new InvalidServerGroupException();
            }
            groups.add(group);
        }

        // Now do the actual add:
        for (ManagedServerGroup group : groups) {
            UserManager.grantServerGroupPermission(targetUser, group.getId());
        }

        // Follow up with a call to addDefaultSystemGroups if setDefault is true:
        if (setDefault.booleanValue()) {
            addDefaultSystemGroups(loggedInUser, login, sgNames);
        }

        return 1;
    }

    /**
     * Return the current value of the createDefaultSystemGroup settnig
     * @param loggedInUser The current user
     * Must be org_admin.
     * @return Returns 1 if successful (exception otherwise)
     *
     * @xmlrpc.doc Returns the current value of the CreateDefaultSystemGroup setting.
     * If True this will cause there to be a system group created (with the same name
     * as the user) every time a new user is created, with the user automatically given
     * permission to that system group and the system group being set as the default
     * group for the user (so every time the user registers a system it will be
     * placed in that system group by default). This can be useful if different
     * users will administer different groups of servers in the same organization.
     * Can only be called by an org_admin.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.returntype #return_int_success()
     */
    public boolean getCreateDefaultSystemGroup(User loggedInUser) {
        //Logged in user must be an org admin.
        ensureOrgAdmin(loggedInUser);

        return loggedInUser.getOrg().getOrgConfig().isCreateDefaultSg();
    }

    /**
     * Return the current value of the createDefaultSystemGroup settnig
     * @param loggedInUser The current user
     * Must be org_admin.
     * @param createDefaultSystemGroup The value to set
     * @return Returns 1 if successful (exception otherwise)
     *
     * @xmlrpc.doc Sets the value of the CreateDefaultSystemGroup setting.
     * If True this will cause there to be a system group created (with the same name
     * as the user) every time a new user is created, with the user automatically given
     * permission to that system group and the system group being set as the default
     * group for the user (so every time the user registers a system it will be
     * placed in that system group by default). This can be useful if different
     * users will administer different groups of servers in the same organization.
     * Can only be called by an org_admin.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("boolean", "createDefaultSystemGruop",
     * "True if we should automatically create system groups, false otherwise.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int setCreateDefaultSystemGroup(User loggedInUser,
            Boolean createDefaultSystemGroup) {
        //Logged in user must be an org admin.
        ensureOrgAdmin(loggedInUser);

        loggedInUser.getOrg().getOrgConfig().setCreateDefaultSg(createDefaultSystemGroup);
        return 1;

    }

    /**
     * @param loggedInUser The current user
     * @param login User to modify.
     * @param readOnly readOnly flag to set
     * @return 1 (should always succeed)
     * @xmlrpc.doc Sets whether the target user should have only read-only API access or
     * standard full scale access.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.param #param_desc("boolean", "readOnly", "Sets whether the target user should
     * have only read-only API access or standard full scale access.")
     * @xmlrpc.returntype #return_int_success()
     */
    public int setReadOnly(User loggedInUser, String login, Boolean readOnly) {
        //Logged in user must be an org admin.
        ensureOrgAdmin(loggedInUser);

        User targetUser = XmlRpcUserHelper.getInstance().lookupTargetUser(
                loggedInUser, login);

        if (!targetUser.isReadOnly()) {
            if (readOnly && targetUser.hasRole(RoleFactory.ORG_ADMIN) &&
                    targetUser.getOrg().numActiveOrgAdmins() < 2) {
                throw new InvalidOperationException("error.readonly_org_admin",
                        targetUser.getOrg().getName());
            }
            if (readOnly && targetUser.hasRole(RoleFactory.SAT_ADMIN) &&
                    SatManager.getActiveSatAdmins().size() < 2) {
                throw new InvalidOperationException("error.readonly_sat_admin");
            }
        }
        targetUser.setReadOnly(readOnly);
        return 1;
    }

    /**
     * @param loggedInUser The current user
     * @param login User to modify
     * @param value value to enable/disable errata mail notifications
     * @return Returns 1 if successful (exception thrown otherwise)
     * @xmlrpc.doc Enables/disables errata mail notifications for a specific user.
     * @xmlrpc.param #param("string", "sessionKey")
     * @xmlrpc.param #param_desc("string", "login", "User's login name.")
     * @xmlrpc.param #param_desc("boolean", "value", "True for enabling
     * errata notifications, False for disabling")
     * @xmlrpc.returntype #return_int_success()
     */
    public int setErrataNotifications(User loggedInUser, String login, Boolean value) {
        //Logged in user must be an org admin.
        ensureOrgAdmin(loggedInUser);

        User targetUser = XmlRpcUserHelper.getInstance().lookupTargetUser(
                loggedInUser, login);
        targetUser.setEmailNotify(BooleanUtils.toIntegerObject(value));
        return 1;
    }
}
