All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.enterprise.security.auth.realm.file.FileRealm Maven / Gradle / Ivy

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2010 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.util.*;
import java.util.logging.Level;
import java.io.*;
import java.security.*;
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.util.*;
import com.sun.enterprise.security.auth.realm.IASRealm;
import com.sun.enterprise.security.common.Util;
import com.sun.enterprise.util.Utility;
import java.nio.charset.Charset;
import org.glassfish.internal.api.SharedSecureRandom;
import org.jvnet.hk2.annotations.Service;


/**
 * Realm wrapper for supporting file password authentication.
 *
 * 

In addition to the basic realm functionality, this class provides * administration methods for the file realm. * *

Format of the keyfile used by this class is one line per user * containing username;password;groups where: *

    *
  • username - Name string. *
  • password - A salted SHA hash (SSHA) of the user password. *
  • groups - A comma separated list of group memberships. *
* *

The file realm needs the following properties in its configuration: *

    *
  • file - Full path to the keyfile to load *
  • jaas-ctx - JAAS context name used to access LoginModule for * authentication. *
* * @author Harry Singh * @author Jyri Virkki * @author Shing Wai Chan */ @Service public final 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 final Hashtable userTable = new Hashtable(); // user=>FileRealmUser private final Hashtable groupSizeMap = new Hashtable(); // maps of groups with value cardinality of group //private boolean constructed = false; /** * Constructor. * *

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. * *

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. * *

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. * *

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. * *

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, char[] password) { FileRealmUser ud = 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(), Utility.convertCharArrayToByteArray(password, Charset.defaultCharset().displayName())); } 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 * [email protected]. 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; iThis 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. * *

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(char[] pwd) throws IASSecurityException { if (Arrays.equals(null, pwd)) { String msg = sm.getString("filerealm.emptypwd"); throw new IASSecurityException(msg); } for(char c:pwd) { if (Character.isSpaceChar(c)) { String msg = sm.getString("filerealm.badspacespwd"); throw new IASSecurityException(msg); } } } /** * Validates syntax of a group name. * *

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. * *

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 uval : userTable.entrySet()) { String entry = encodeUser(uval.getKey(), uval.getValue()); 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(); } } } if (_logger.isLoggable(Level.FINE)) { _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 = groupSizeMap.get(groupList[i]); groupSizeMap.put(groupList[i],Integer.valueOf((groupSize != null) ? (groupSize.intValue() + 1): 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 = groupSizeMap.get(groupList[i]); if (groupSize != null) { int gpSize = groupSize.intValue() - 1; if (gpSize > 0) { groupSizeMap.put(groupList[i], Integer.valueOf(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); //Adding this feature of creating the empty keyfile // to satisfy some admin requirements. //allow the file creation only if it is inside glassfish domain-config File filePath = new File(file); if ((file != null) && !filePath.exists()) { try { if (file.indexOf("domains") > 0 && file.indexOf("config") > 0) { filePath.createNewFile(); } } catch (IOException ex) { //ignore any exception, so the code below //will then throw No such file or directory } } BufferedReader input = null; try { if (Util.isEmbeddedServer()) { String embeddedFilePath = Util.writeConfigFileToTempDir(file).getAbsolutePath(); this.setProperty(PARAM_KEYFILE, embeddedFilePath); input = new BufferedReader(new FileReader(embeddedFilePath)); } else { 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, Integer.valueOf((groupSize != null) ? (groupSize.intValue() + 1) : 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, char[] 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, char[] pwd) throws IASSecurityException { assert (user != null); //Copy the password to another reference before storing it to the //instance field. byte[] pwdBytes = null; try { pwdBytes = Utility.convertCharArrayToByteArray(pwd, Charset.defaultCharset().displayName()); } catch(Exception ex) { throw new IASSecurityException(ex); } SecureRandom rng=SharedSecureRandom.get(); 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





© 2015 - 2025 Weber Informatics LLC | Privacy Policy