/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */


package com.sun.enterprise.security.auth.realm.file;

import java.lang.*;
import java.util.*;
import java.util.logging.Logger;
import java.util.logging.Level;
import com.sun.logging.LogDomains;
import java.io.*;
import java.security.*;
import javax.security.auth.login.*;
import com.sun.enterprise.security.auth.realm.User;
import com.sun.enterprise.security.auth.realm.Realm;
import com.sun.enterprise.security.auth.realm.BadRealmException;
import com.sun.enterprise.security.auth.realm.NoSuchUserException;
import com.sun.enterprise.security.auth.realm.NoSuchRealmException;
import com.sun.enterprise.security.auth.realm.AuthenticationHandler;
import com.sun.enterprise.security.auth.realm.InvalidOperationException;
import com.sun.enterprise.server.*;
import com.sun.enterprise.security.RealmConfig;
import com.sun.enterprise.security.util.*;
import com.sun.enterprise.security.auth.realm.IASRealm;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;


/**
 * Realm wrapper for supporting file password authentication.
 *
 * <P>In addition to the basic realm functionality, this class provides
 * administration methods for the file realm.
 *
 * <P>Format of the keyfile used by this class is one line per user
 * containing <code>username;password;groups</code> where:
 * <ul>
 *   <li>username - Name string.
 *   <li>password - A salted SHA hash (SSHA) of the user password.
 *   <li>groups - A comma separated list of group memberships.
 * </ul>
 *
 * <P>The file realm needs the following properties in its configuration:
 * <ul>
 *   <li>file - Full path to the keyfile to load
 *   <li>jaas-ctx - JAAS context name used to access LoginModule for
 *       authentication.
 * </ul>
 *
 * @author Harry Singh
 * @author Jyri Virkki
 * @author Shing Wai Chan
 */

final public class FileRealm extends IASRealm
{
    // Descriptive string of the authentication type of this realm.
    public static final String AUTH_TYPE = "filepassword";

    // These are property names which should be in auth-realm in server.xml
    public static final String PARAM_KEYFILE="file";

    // Separators in keyfile (user;pwd-info;group[,group]*)
    private static final String FIELD_SEP=";";
    private static final String GROUP_SEP=",";
    private static final String COMMENT="#";

    // Valid non-alphanumeric/whitespace chars in user/group name
    public static final String MISC_VALID_CHARS="_-.";
    
    // Number of bytes of salt for SSHA
    private static final int SALT_SIZE=8;
    
    // Contains cache of keyfile data
    private Map userTable;  // user=>FileRealmUser
    private Hashtable groupSizeMap; // maps of groups with value cardinality of group

    private boolean constructed = false;
    

    /**
     * Constructor.
     *
     * <P>The created FileRealm instance is not registered in the
     * Realm registry. This constructor can be used by admin tools
     * to create a FileRealm instance which can be edited by adding or
     * removing users and then saved to disk, without affecting the
     * installed realm instance.
     *
     * <P>The file provided should always exist. A default (empty) keyfile
     * is installed with the server so this should always be the case
     * unless the user has manually deleted this file.  If this file
     * path provided does not point to an existing file this constructor
     * will first attempt to create it. If this succeeds the constructor
     * returns normally and an empty keyfile will have been created; otherwise
     * an exception is thrown.
     *
     * @param keyfile Full path to the keyfile to read for user data.
     * @exception BadRealmException If the configuration parameters
     *     identify a corrupt realm.
     * @exception NoSuchRealmException If the configuration parameters
     *     specify a realm which doesn't exist.
     *
     */
    public FileRealm(String keyfile)
         throws BadRealmException, NoSuchRealmException
    {
        File fp = new File(keyfile);
                                // if not existent, try to create
        if (!fp.exists()) {
            FileOutputStream fout = null;
            try {
                fout =  new FileOutputStream(fp);
                fout.write("\n".getBytes());
            } catch (Exception e) {
                String msg = sm.getString("filerealm.noaccess", e.toString());
                throw new BadRealmException(msg);
            } finally {
                if (fout != null) {
                    try {
                        fout.close();
                    } catch(Exception ex) {
                        // ignore close exception
                    }
                }
            }
        }
        
        constructed = true;
        Properties p = new Properties();
        p.setProperty(PARAM_KEYFILE, keyfile);
        p.setProperty(IASRealm.JAAS_CONTEXT_PARAM, "ignore");
        this.init(p);
    }

    
    /**
     * Constructor.
     *
     * <P>Do not use directly.
     */
    public FileRealm()
    {
    }

    
    /**
     * Initialize a realm with some properties.  This can be used
     * when instantiating realms from their descriptions.  This
     * method is invoked from Realm during initialization.
     *
     * @param props Initialization parameters used by this realm.
     * @exception BadRealmException If the configuration parameters
     *     identify a corrupt realm.
     * @exception NoSuchRealmException If the configuration parameters
     *     specify a realm which doesn't exist.
     *
     */
    protected void init(Properties props)
        throws BadRealmException, NoSuchRealmException
    {
        super.init(props);
        String file = props.getProperty(PARAM_KEYFILE);
        if (file == null) {
            String msg = sm.getString("filerealm.nofile");
            throw new BadRealmException(msg);
        }
        this.setProperty(PARAM_KEYFILE, file);
        
        String jaasCtx = props.getProperty(IASRealm.JAAS_CONTEXT_PARAM);
        if (jaasCtx == null) {
            String msg = sm.getString("filerealm.nomodule");
            throw new BadRealmException(msg);
        }
        this.setProperty(IASRealm.JAAS_CONTEXT_PARAM, jaasCtx);

        if (_logger.isLoggable(Level.FINE)) {
            _logger.fine("FileRealm : "+PARAM_KEYFILE+"="+file);
            _logger.fine("FileRealm : "+IASRealm.JAAS_CONTEXT_PARAM+"="+
                     jaasCtx);
        }
        
        loadKeyFile();
    }


    /**
     * Returns a short (preferably less than fifteen characters) description
     * of the kind of authentication which is supported by this realm.
     *
     * @return Description of the kind of authentication that is directly
     *     supported by this realm.
     */
    public String getAuthType()
    {
        return AUTH_TYPE;
    }
    

    /**
     * Returns names of all the users in this particular realm.
     *
     * @return enumeration of user names (strings)
     * @exception BadRealmException if realm data structures are bad
     */
    public Enumeration getUserNames()
         throws BadRealmException
    {
        return (new Vector(userTable.keySet())).elements(); // ugh
    }


    /**
     * Returns the information recorded about a particular named user.
     *
     * @param name Name of the user whose information is desired.
     * @return The user object.
     * @exception NoSuchUserException if the user doesn't exist.
     * @exception BadRealmException if realm data structures are bad.
     */
    public User getUser(String name)
        throws NoSuchUserException
    {
        FileRealmUser u = (FileRealmUser)userTable.get(name);
        if (u == null) {
            String msg = sm.getString("filerealm.nouser", name);
            throw new NoSuchUserException(msg);
        }
        return u;
    }
    

    /**
     * Returns names of all the groups in this particular realm.
     * Note that this will not return assign-groups.
     *
     * @return enumeration of group names (strings)
     * @exception BadRealmException if realm data structures are bad
     */
    public Enumeration getGroupNames()
        throws BadRealmException
    {
        return groupSizeMap.keys();
    }

    
    /**
     * Returns the name of all the groups that this user belongs to.
     * @param username Name of the user in this realm whose group listing
     *     is needed.
     * @return Enumeration of group names (strings).
     * @exception InvalidOperationException thrown if the realm does not
     *     support this operation - e.g. Certificate realm does not support
     *     this operation.
     */
    public Enumeration getGroupNames(String username)
        throws NoSuchUserException
    {
        FileRealmUser ud = (FileRealmUser)userTable.get(username);
        if (ud == null) {
            String msg = sm.getString("filerealm.nouser", username);
            throw new NoSuchUserException(msg);
        }

        String[] groups = ud.getGroups();
        groups = addAssignGroups(groups);
        Vector v = new Vector();
        if (groups != null) {
            for (int i = 0; i < groups.length; i++) {
               v.add(groups[i]);
            }
        }
        return v.elements();
    }
    

    /**
     * Refreshes the realm data so that new users/groups are visible.
     *
     * <P>A new FileRealm instance is created and initialized from the
     * keyfile on disk. The new instance is installed in the Realm registry
     * so future Realm.getInstance() calls will obtain the new data. Any
     * existing references to this instance (e.g. in active LoginModule
     * sessions) are unaffected.
     *
     * @exception BadRealmException if realm data structures are bad
     *
     */
    public void refresh()
         throws BadRealmException
    {
        if (_logger.isLoggable(Level.FINE)) {
            _logger.fine("Reloading file realm data.");
        }

        FileRealm newRealm = new FileRealm();

        try {
            newRealm.init(getProperties());
            Realm.updateInstance(newRealm, this.getName());
        } catch (Exception e) {
            throw new BadRealmException(e.toString());
        }
    }


    /**
     * Authenticates a user.
     *
     * <P>This method is invoked by the FileLoginModule in order to
     * authenticate a user in the file realm. The authentication decision
     * is kept within the realm class implementation in order to keep
     * the password cache in a single location with no public accessors,
     * to simplify future improvements.
     *
     * @param user Name of user to authenticate.
     * @param password Password provided by client.
     * @returns Array of group names the user belongs to, or null if
     *      authentication fails.
     * @throws LoginException If there are errors during authentication.
     *
     */
    public String[] authenticate(String user, String password)
    {
        FileRealmUser ud = (FileRealmUser)userTable.get(user);
        if (ud == null) {
            if (_logger.isLoggable(Level.FINE)) {
                _logger.fine("No such user: [" + user + "]");
            }
            return null;
        }

        boolean ok = false;
        try {
            ok = SSHA.verify(ud.getSalt(), ud.getHash(), password.getBytes());

        } catch (Exception e) {
            _logger.fine("File authentication failed: "+e.toString());
            return null;
        }

        if (!ok) {
            if (_logger.isLoggable(Level.FINE)) {
                _logger.fine("File authentication failed for: ["+user+"]");
            }
            return null;
        }
        
        String[] groups = ud.getGroups();
        groups = addAssignGroups(groups);
        return groups;
    }

    

    //---------------------------------------------------------------------
    // File realm maintenance methods for admin.


    /**
     * Return false if any char of the string is not alphanumeric or space
     * or other permitted character.
     * For a username it will allow an @ symbol. To allow for the case of type
     * <i>username@foo.com</i>. It will not allow the same symbol for a group name 
     * @param String the name to be validated
     * @param boolean true if the string is a username, false if it is 
     * a group name
     * 
     */
    private static boolean isValid(String s, boolean userName)
    {
        for (int i=0; i<s.length(); i++) {
            char c = s.charAt(i);
            if (!Character.isLetterOrDigit(c) &&
                !Character.isWhitespace(c) &&
                MISC_VALID_CHARS.indexOf(c) == -1) {
                if (userName && (c == '@')){
                    continue;
                }
                return false;
            }
        }
        return true;
    }

    
    /**
     * Validates syntax of a user name.
     *
     * <P>This method throws an exception if the provided value is not
     * valid. The message of the exception provides a reason why it is
     * not valid. This is used internally by add/modify User to
     * validate the client-provided values. It is not necessary for
     * the client to call these methods first. However, these are
     * provided as public methods for convenience in case some client
     * (e.g. GUI client) wants to provide independent field validation
     * prior to calling add/modify user.
     *
     * @param name User name to validate.
     * @throws IASSecurityException Thrown if the value is not valid.
     *
     */
    public static void validateUserName(String name)
        throws IASSecurityException
    {
        if (name == null || name.length() == 0) {
            String msg = sm.getString("filerealm.noname");
            throw new IASSecurityException(msg);
        }

        if (!isValid(name, true)) {
            String msg = sm.getString("filerealm.badname", name);
            throw new IASSecurityException(msg);
        }

        if (!name.equals(name.trim())) {
            String msg = sm.getString("filerealm.badspaces", name);
            throw new IASSecurityException(msg);
        }
    }


    /**
     * Validates syntax of a password.
     *
     * <P>This method throws an exception if the provided value is not
     * valid. The message of the exception provides a reason why it is
     * not valid. This is used internally by add/modify User to
     * validate the client-provided values. It is not necessary for
     * the client to call these methods first. However, these are
     * provided as public methods for convenience in case some client
     * (e.g. GUI client) wants to provide independent field validation
     * prior to calling add/modify user.
     *
     * @param pwd Password to validate.
     * @throws IASSecurityException Thrown if the value is not valid.
     *
     */
    public static void validatePassword(String pwd)
        throws IASSecurityException
    {
        if (pwd == null) {
            String msg = sm.getString("filerealm.emptypwd");
            throw new IASSecurityException(msg);
        }

        if (!pwd.equals(pwd.trim())) {
            String msg = sm.getString("filerealm.badspacespwd");
            throw new IASSecurityException(msg);
        }
    }


    /**
     * Validates syntax of a group name.
     *
     * <P>This method throws an exception if the provided value is not
     * valid. The message of the exception provides a reason why it is
     * not valid. This is used internally by add/modify User to
     * validate the client-provided values. It is not necessary for
     * the client to call these methods first. However, these are
     * provided as public methods for convenience in case some client
     * (e.g. GUI client) wants to provide independent field validation
     * prior to calling add/modify user.
     *
     * @param group Group name to validate.
     * @throws IASSecurityException Thrown if the value is not valid.
     *
     */
    public static void validateGroupName(String group)
        throws IASSecurityException
    {
        if (group == null || group.length() == 0) {
            String msg = sm.getString("filerealm.nogroup");
            throw new IASSecurityException(msg);
        }

        if (!isValid(group, false)) {
            String msg = sm.getString("filerealm.badchars", group);
            throw new IASSecurityException(msg);
        }
        
        if (!group.equals(group.trim())) {
            String msg = sm.getString("filerealm.badspaces", group);
            throw new IASSecurityException(msg);
        }
    }

    
    /**
     * Validates syntax of a list of group names.
     *
     * <P>This is equivalent to calling validateGroupName on every element
     * of the groupList.
     *
     * @param groupList Array of group names to validate.
     * @throws IASSecurityException Thrown if the value is not valid.
     *     
     *
     */
    public static void validateGroupList(String[] groupList)
        throws IASSecurityException
    {
        if (groupList == null || groupList.length == 0) {
            return;             // empty list is ok
        }

        for (int i=0; i<groupList.length; i++) {
            validateGroupName(groupList[i]);
        }
        
    }

    
    /**
     * Adds new user to file realm. User cannot exist already.
     *
     * @param name User name.
     * @param password Cleartext password for the user.
     * @param groupList List of groups to which user belongs.
     * @throws BadRealmException If there are problems adding user.
     *
     */
    public synchronized void addUser(String name, String password,
                        String[] groupList)
        throws BadRealmException, IASSecurityException
    {
        validateUserName(name);
        validatePassword(password);
        validateGroupList(groupList);
        
        if (userTable.containsKey(name)) {
            String msg = sm.getString("filerealm.dupuser", name);
            throw new BadRealmException(msg);
        }

        addGroupNames(groupList);        
        FileRealmUser ud = createNewUser(name, password, groupList);
        userTable.put(name, ud);
    }


    /**
     * Remove user from file realm. User must exist.
     *
     * @param name User name.
     * @throws NoSuchUserException If user does not exist.
     *
     */
    public synchronized void removeUser(String name)
        throws NoSuchUserException
    {
        if (!userTable.containsKey(name)) {
            String msg = sm.getString("filerealm.nouser", name);
            throw new NoSuchUserException(msg);
        }

        FileRealmUser oldUser = (FileRealmUser)userTable.get(name);
        userTable.remove(name);
        reduceGroups(oldUser.getGroups());
    }


    /**
     * Update data for an existing user. User must exist. This is equivalent
     * to calling removeUser() followed by addUser().
     *
     * @param name User name.
     * @param password Cleartext password for the user.
     * @param groupList List of groups to which user belongs.
     * @throws BadRealmException If there are problems adding user.
     * @throws NoSuchUserException If user does not exist.
     * @deprecated
     *
     */
    public synchronized void updateUser(String name, String password,
                           String[] groups)
        throws NoSuchUserException, BadRealmException,
                               IASSecurityException
    {
        updateUser(name, name, password, groups);
    }
    

    /**
     * Update data for an existing user. User must exist.
     *
     * @param name Current name of the user to update.
     * @param newName New name to give this user. It can be the same as
     *     the original name. Otherwise it must be a new user name which
     *     does not already exist as a user.
     * @param password Cleartext password for the user. If non-null the user
     *     password is changed to this value. If null, the original password
     *     is retained.
     * @param groupList List of groups to which user belongs.
     * @throws BadRealmException If there are problems adding user.
     * @throws NoSuchUserException If user does not exist.
     *
     */
    public synchronized void updateUser(String name, String newName, String password,
                           String[] groups)
        throws NoSuchUserException, BadRealmException,
                               IASSecurityException
    {
                                // user to modify must exist first
        validateUserName(name);
        if (!userTable.containsKey(name)) {
            String msg = sm.getString("filerealm.nouser", name);
            throw new NoSuchUserException(msg);
        }

                                // do general validation
        validateUserName(newName);
        validateGroupList(groups);
        if (password != null) { // null here means re-use previous so is ok
            validatePassword(password);
        }
        
                                // can't duplicate unless modifying itself
        if (!name.equals(newName) && userTable.containsKey(newName)) {
            String msg = sm.getString("filerealm.dupuser", name);
            throw new BadRealmException(msg);
        }

        
        FileRealmUser oldUser = (FileRealmUser)userTable.get(name);
        assert (oldUser != null);
        
                                // create user using new name
        FileRealmUser newUser = new FileRealmUser(newName);
        
                                // set groups as provided by parameter
        changeGroups(oldUser.getGroups(), groups);
        newUser.setGroups(groups);
        
                                // use old password if no new pwd given
        if (password==null) {
            newUser.setSalt(oldUser.getSalt());
            newUser.setHash(oldUser.getHash());
            
        } else {
            setPassword(newUser, password);
        }
        
        userTable.remove(name);
        userTable.put(newName, newUser);
    }
    
        
    /**
     * Write keyfile data out to disk. The file generation is sychronized
     * within this class only, caller is responsible for any other
     * file locking or revision management as deemed necessary.
     *
     * @param filename The name of the output file to create. 
     * @throws IOException If write fails.
     *
     */
    public void writeKeyFile(String filename)
         throws IOException
    {
        synchronized(FileRealm.class) {
            FileOutputStream out = null;
            try {
                out = new FileOutputStream(filename);

                Iterator names = userTable.keySet().iterator();
                while (names.hasNext()) {
                
                    String name = (String)names.next();
                    FileRealmUser ud = (FileRealmUser)userTable.get(name);

                    String entry = encodeUser(name, ud);
                    out.write(entry.getBytes());
                }
            } catch (IOException e) {
                throw e;

            } catch (Exception e) {
                String msg = sm.getString("filerealm.badwrite", e.toString());
                throw new IOException(msg);
            } finally {
                if (out != null) {
                    out.close();
                }
            }
        }

        _logger.fine("Done writing "+filename);
    }

    
    //---------------------------------------------------------------------
    // Private methods.

    
    /**
     * Add group names to the groups table. It is assumed all entries are
     * valid group names.
     *
     */
    private void addGroupNames(String[] groupList) {
        if (groupList != null) {
            for (int i=0; i < groupList.length; i++) {
                Integer groupSize = (Integer)groupSizeMap.get(groupList[i]);
                groupSizeMap.put(groupList[i],
                    (groupSize != null) ?
                    new Integer(groupSize.intValue() + 1): new Integer(1));
            }
        }
    }

    /**
     * This method reduces the group size by 1 and remove group name from
     * internal group list if resulting group size is 0.
     */
    private void reduceGroups(String[] groupList) {
        if (groupList != null) {
            for (int i=0; i < groupList.length; i++) {
                Integer groupSize = (Integer)groupSizeMap.get(groupList[i]);
                if (groupSize != null) {
                    int gpSize = groupSize.intValue() - 1;
                    if (gpSize > 0) {
                        groupSizeMap.put(groupList[i], new Integer(gpSize));
                    } else {
                        groupSizeMap.remove(groupList[i]);
                    }
                }
            }
        }
    }

    /**
     * This method update the internal group list.
     */
    private void changeGroups(String[] oldGroupList, String[] newGroupList) {
        addGroupNames(newGroupList);
        reduceGroups(oldGroupList);
    }
    
    
    /**
     * Load keyfile from config and populate internal cache.
     *
     */
    private void loadKeyFile() throws BadRealmException
    {
        String file = this.getProperty(PARAM_KEYFILE);

        _logger.fine("Reading file realm: "+file);

        userTable = new Hashtable();
        groupSizeMap = new Hashtable();
        BufferedReader input = null;
        
        try {
            input = new BufferedReader(new FileReader(file));

            while (input.ready()) {
                
                String line = input.readLine();
                if (!line.startsWith(COMMENT) &&
                    line.indexOf(FIELD_SEP) > 0) {
                    FileRealmUser ud = decodeUser(line, groupSizeMap);
                    userTable.put(ud.getName(), ud);
                }
            }
        } catch (Exception e) {
            _logger.log(Level.WARNING, "filerealm.readerror", e);
            throw new BadRealmException(e.toString());
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch(Exception ex) {
                }
            }
        }
    }
    
    
    /**
     * Encodes one user entry containing info stored in FileRealmUser object.
     *
     * @param name User name.
     * @param ud User object containing info.
     * @returns String containing a line with encoded user data.
     * @throws IASSecurityException Thrown on failure.
     *
     */
    private static String encodeUser(String name, FileRealmUser ud)
    {
        StringBuffer sb = new StringBuffer();
        String cryptPwd = null;

        sb.append(name);
        sb.append(FIELD_SEP);

        String ssha = SSHA.encode(ud.getSalt(), ud.getHash());

        sb.append(ssha);
        sb.append(FIELD_SEP);

        String[] groups = ud.getGroups();
        if (groups != null) {
            for (int grp = 0; grp < groups.length; grp++) {
                if (grp > 0) {
                    sb.append(GROUP_SEP);
                }
                sb.append((String)groups[grp]);
            }
        }
        sb.append("\n");
        return sb.toString();
    }


    /**
     * Decodes a line from the keyfile.
     *
     * @param encodedLine A line from the keyfile containing user data.
     * @param newGroupSizeMap Groups found in the encodedLine are added to
     *      this map.
     * @returns FileRealmUser Representing the loaded user.
     * @throws IASSecurityException Thrown on failure.
     *
     */
    private static FileRealmUser decodeUser(String encodedLine,
                                            Map newGroupSizeMap)
        throws IASSecurityException
    {
        StringTokenizer st = new StringTokenizer(encodedLine, FIELD_SEP);

        String user = null;
        String pwdInfo = null;
        String groupList = null;

        try {                   // these must be present
            user = st.nextToken();
            pwdInfo = st.nextToken();
        } catch (Exception e) {
            String msg = sm.getString("filerealm.syntaxerror", encodedLine);
            throw new IASSecurityException(msg);
        }
        
        if (st.hasMoreTokens()) { // groups are optional
            groupList = st.nextToken();
        }

        byte[] hash = new byte[20];
        byte[] salt = SSHA.decode(pwdInfo, hash);

        FileRealmUser ud = new FileRealmUser(user);
        ud.setHash(hash);
        ud.setSalt(salt);
        
        Vector membership = new Vector();

        if (groupList != null) {
            StringTokenizer gst = new StringTokenizer(groupList,
                                                      GROUP_SEP);
            while (gst.hasMoreTokens()) {
                String g = gst.nextToken();
                membership.add(g);
                Integer groupSize = (Integer)newGroupSizeMap.get(g);
                newGroupSizeMap.put(g, (groupSize != null) ?
                    new Integer(groupSize.intValue() + 1) : new Integer(1));
            }
        }
        ud.setGroups(membership);
        return ud;
    }

    
    /**
     * Produce a user with given data.
     *
     * @param name User name.
     * @param pwd Cleartext password.
     * @param groups Group membership.
     * @returns FileRealmUser Representing the created user.
     * @throws IASSecurityException Thrown on failure.
     *
     */
    private static FileRealmUser createNewUser(String name, String pwd,
                                               String[] groups)
        throws IASSecurityException
    {
        FileRealmUser ud = new FileRealmUser(name);

        if (groups == null) {
            groups = new String[0];
        }
        ud.setGroups(groups);
        
        setPassword(ud, pwd);
     
        return ud;
    }


    /**
     * Sets the password in a user object. Of course the password is not
     * really stored so a salt is generated, hash computed, and these two
     * values are stored in the user object provided.
     *
     */
    private static void setPassword(FileRealmUser user, String pwd)
        throws IASSecurityException
    {
        assert (user != null);
        byte[] pwdBytes = pwd.getBytes();
        
        SecureRandom rng=new SecureRandom();
        byte[] salt=new byte[SALT_SIZE];
        rng.nextBytes(salt);
        user.setSalt(salt);

        byte[] hash = SSHA.compute(salt, pwdBytes);
        user.setHash(hash);
    }



    /**
     * Test method. Not for production use.
     *
     */
    public static void main(String[] args)
    {
        if (args.length==0) {
            help();
        }

        try {
            if ("-c".equals(args[0])) {
                String[] groups=new String[0];
                if (args.length>3) {
                    groups=new String[args.length-3];
                    for (int i=3; i<args.length; i++) {
                        groups[i-3]=args[i];
                    }
                }
                FileRealmUser ud = createNewUser(args[1], args[2], groups);
                String out=encodeUser(args[1], ud);
                System.out.println(out);
                
                FileRealmUser u=decodeUser(out, new Hashtable());
                System.out.println("verifies: "+
                                   SSHA.verify(u.getSalt(), u.getHash(),
                                               args[2].getBytes()));

            } else if ("-v".equals(args[0])) {
                FileRealmUser u=decodeUser(args[2], new Hashtable());
                System.out.println("user: "+u.getName());
                System.out.println("verifies: "+
                                   SSHA.verify(u.getSalt(), u.getHash(),
                                               args[1].getBytes()));
            }
        } catch (Exception e) {
            e.printStackTrace(System.out);
        }
    } 
    /**
     * Show help for the test command line tool.
     *
     */
    private static void help()
    {
        System.out.println("FileRealm -c <name  <pwd  [group]*");
        System.out.println("FileRealm -v <pwd  `output of -c`");
        System.exit(1);
    }



}
