![JAR search and dependency download from the Maven repository](/logo.png)
com.sun.enterprise.security.auth.realm.file.FileRealm Maven / Gradle / Ivy
/*
* 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.util.*;
import java.util.logging.Level;
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.util.*;
import com.sun.enterprise.security.auth.realm.IASRealm;
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
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.
*
* 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, 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
* [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(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.
*
*
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 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