package examples.security.rdbmsrealm;
import examples.security.util.RealmProperties;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.security.Principal;
import java.security.acl.Acl;
import java.security.acl.Group;
import java.security.acl.NotOwnerException;
import java.security.acl.Permission;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import weblogic.security.acl.AclEntryImpl;
import weblogic.security.acl.AclImpl;
import weblogic.security.acl.PermissionImpl;
import weblogic.security.acl.User;
import weblogic.security.acl.UserInfo;
import weblogic.utils.reuse.Factory;
/**
* The RDBMS delegate class. An instance of this class communicates
* with a single database connection. A pool of instacnes is then
* maintained by RDBMSRealm to provide high performance.
*
* @author Copyright (c) 1998-2000 by BEA Systems, Inc. All Rights Reserved.
*/
public class RDBMSDelegate
{
// The following are some names that we look for in the property file.
static final String DRIVER = "driver";
static final String DB_URL = "dbURL";
static final String DB_USER = "dbUser";
static final String DB_PASSWORD = "dbPassword";
/**
* The realm with which this delegate is associated.
*/
protected RDBMSRealm realm;
/**
* The main connection to the database.
*/
protected Connection conn;
// We use multiple connections for JDBC drivers that can't handle
// multiple open result sets on a single connection.
protected Connection conn2;
protected Connection conn3;
protected Connection conn4;
private RealmProperties properties;
// We maintain a number of prepared SQL statements for performing
// database lookups.
private PreparedStatement getUserStmt;
private PreparedStatement getGroupMembersStmt;
private PreparedStatement getPermissionStmt;
private PreparedStatement getAclEntriesStmt;
private PreparedStatement getUsersStmt;
private PreparedStatement getGroupsStmt;
private PreparedStatement getAclsStmt;
private PreparedStatement getPermissionsStmt;
private PreparedStatement newUserStmt;
private PreparedStatement addGroupMemberStmt;
private PreparedStatement removeGroupMemberStmt;
private PreparedStatement deleteUserStmt1;
private PreparedStatement deleteUserStmt2;
private PreparedStatement deleteUserStmt3;
private PreparedStatement deleteGroupStmt1;
private PreparedStatement deleteGroupStmt2;
/**
* Determine whether or not we need to create a new SQL statement
* for recursive calls to getGroup. Some servers need this, and
* some don't.
*/
private boolean getGroupNewStatement;
/**
* This is the bogus owner associated with all ACLs found in the
* database.
*/
protected Principal aclOwner = new User("unperson");
/**
* A shorthand convenience function for preparing an SQL statement.
*
* @param name the name of the statement to prepare
*/
protected PreparedStatement prepare(String propKey)
throws SQLException, RDBMSException
{
String sqlStr = properties.get(propKey);
if (sqlStr == null)
{
throw new RDBMSException("realm initialization failed, could not find property '" +
propKey + "' in properties file " +
RDBMSRealm.RDBMS_PROPS);
}
return conn.prepareStatement(sqlStr);
}
/**
* Create a new delegate, associated with the given realm.
*
* @exception RDBMSException an error occurred in fetching
* properties or communicating with the database
*/
protected RDBMSDelegate(RDBMSRealm realm)
{
this.realm = realm;
if (realm.log != null)
realm.log.debug("loading realm properties: " + RDBMSRealm.RDBMS_PROPS);
try
{
properties = new RealmProperties(RDBMSRealm.class, RDBMSRealm.RDBMS_PROPS);
}
catch (IOException e)
{
throw new RDBMSException("could not find properties", e);
}
getGroupNewStatement = properties.getBoolean("getGroupNewStatement", true);
String action = "properties.get"; // used to help diagnose an exception
try
{
String driver = properties.get(DRIVER);
if (realm.log != null)
realm.log.debug("driver is " + driver);
action = "driver.newInstance()";
Class.forName(driver).newInstance();
action = "properties.get";
String url = properties.get(DB_URL);
String user = properties.get(DB_USER);
String passwd = properties.get(DB_PASSWORD);
if (realm.log != null)
realm.log.debug("connecting to " + url);
action = "DriverManager.getConnection";
conn = DriverManager.getConnection(url, user, passwd);
}
catch (Exception e)
{
throw new RDBMSException("realm initialization failed, action '" +
action + "', ", e);
}
// pkey is only used to help diagnose an SQLException; prepare()
// generates an RDBMSException if the property key is not found.
String pkey = null;
try
{
if (realm.log != null)
realm.log.debug("preparing statements from " + RDBMSRealm.RDBMS_PROPS);
getUserStmt = prepare(pkey = "getUser");
getAclEntriesStmt = prepare(pkey = "getAclEntries");
getUsersStmt = prepare(pkey = "getUsers");
getGroupsStmt = prepare(pkey = "getGroups");
getAclsStmt = prepare(pkey = "getAcls");
getPermissionStmt = prepare(pkey = "getPermission");
getPermissionsStmt = prepare(pkey = "getPermissions");
if (getGroupNewStatement == false)
{
getGroupMembersStmt = prepare(pkey = "getGroupMembers");
}
newUserStmt = prepare(pkey = "newUser");
addGroupMemberStmt = prepare(pkey = "addGroupMember");
removeGroupMemberStmt = prepare(pkey = "removeGroupMember");
deleteUserStmt1 = prepare(pkey = "deleteUser1");
deleteUserStmt2 = prepare(pkey = "deleteUser2");
deleteUserStmt3 = prepare(pkey = "deleteUser3");
deleteGroupStmt1 = prepare(pkey = "deleteGroup1");
deleteGroupStmt2 = prepare(pkey = "deleteGroup2");
}
catch (SQLException se)
{
String sqlStr = properties.get(pkey); // this call will succeed, it has already worked in prepare()
throw new RDBMSException("realm initialization failed, Connection.prepareStatement() failed on statement \"" +
sqlStr + "\", ", se);
}
}
/**
* We use this class to indicate to a caller that a method has
* reached the end of a ResultSet. This is <i>not</i> an indication
* of error; it's just a slightly uncommon (in Java, anyway) way of
* returning more than one value.
*/
protected static class Finished
extends Throwable
{
/**
* The value associated with this object.
*/
private Object value;
/**
* Create a new object, with given value.
*/
Finished(Object value)
{
this.value = value;
}
/**
* Return the value associated with this object.
*/
Object getValue()
{
return value;
}
}
/**
* Get a user from the database, or null if the user doesn't exist.
*/
public User getUser(String name)
throws SQLException
{
if (realm.log != null)
realm.log.debug("getUser(\"" + name + "\")");
getUserStmt.setString(1, name);
ResultSet rs = getUserStmt.executeQuery();
try
{
// If the ResultSet is empty, the user doesn't exist in the
// database.
return rs.next()
? realm.createUser(rs.getString(1), rs.getString(2)) : null;
}
finally
{
// Politely clean up after ourselves.
rs.close();
}
}
/**
* Get all users from the database.
*/
public Enumeration getUsers()
throws SQLException
{
if (realm.log != null)
realm.log.debug("getUsers()");
ResultSet rs = getUsersStmt.executeQuery();
Vector users = new Vector(1);
try
{
while (rs.next())
{
users.addElement(realm.createUser(rs.getString(1), rs.getString(2)));
}
return users.elements();
}
finally
{
rs.close();
}
}
/**
* Get the named group from the database, or null if it doesn't
* exist.
*/
public Group getGroup(String name)
throws SQLException
{
if (realm.log != null)
realm.log.debug("getGroup(\"" + name + "\")");
// If getGroupNewStatement is enabled, this may incur more
// overhead than it needs to, because it creates a new
// PreparedStatement that is only used once.
PreparedStatement stmt = getGroupNewStatement
? prepare("getGroupMembers")
: getGroupMembersStmt;
stmt.setString(1, name);
ResultSet rs = stmt.executeQuery();
try
{
return rs.next() ? getGroupInternal(name, rs) : null;
}
catch (Finished f)
{
// If getGroupInternal threw us a Finished object, we just
// return the value inside it.
return (RDBMSGroup) f.getValue();
}
finally
{
rs.close();
if (getGroupNewStatement)
{
stmt.close();
}
}
}
/**
* Get all groups from the database. Note that in this realm, empty
* groups cannot currently exist.
*/
public Enumeration getGroups()
throws SQLException
{
if (realm.log != null)
realm.log.debug("getGroups()");
ResultSet rs = getGroupsStmt.executeQuery();
Vector groups = new Vector(1);
try
{
// We loop forever until getGroupInternal throws a Finished
// object to let us know that we've reached the end of the
// ResultSet.
if (rs.next())
{
while (true)
{
groups.addElement(getGroupInternal(null, rs));
}
}
}
catch (Finished f)
{
// Add the last element to the set of groups.
groups.addElement(f.getValue());
}
finally
{
rs.close();
}
return groups.elements();
}
public User newUser(String name, String passwd)
throws SQLException, SecurityException
{
if (realm.log != null)
realm.log.debug("newUser(\"" + name + "\", \"" + passwd + "\")");
if (getUser(name) != null)
{
throw new SecurityException("user \"" + name + "\" already exists");
}
newUserStmt.setString(1, name);
newUserStmt.setString(2, passwd);
int rows = newUserStmt.executeUpdate();
if (rows != 1)
{
throw new RDBMSException("insert updated " + rows + " rows (should be 1)");
}
return realm.createUser(name, passwd);
}
public void deleteUser(User user)
throws SQLException
{
String name = user.getName();
deleteUserStmt1.setString(1, name);
deleteUserStmt2.setString(1, name);
deleteUserStmt3.setString(1, name);
deleteUserStmt1.executeUpdate();
deleteUserStmt2.executeUpdate();
deleteUserStmt3.executeUpdate();
}
public void deleteGroup(Group group)
throws SQLException
{
String name = group.getName();
deleteGroupStmt1.setString(1, name);
deleteGroupStmt2.setString(1, name);
deleteGroupStmt1.executeUpdate();
deleteGroupStmt2.executeUpdate();
}
/**
* This method is called both by getGroup and getGroups. It looks
* through the given ResultSet and gathers group members until it
* either hits a differently-named group or the end of the
* ResultSet.
*/
protected Group getGroupInternal(String name, ResultSet rs)
throws Finished, SQLException
{
// All of the other methods in this class with similar names are
// patterned after this one.
// We expect the ResultSet that we are reading to cluster all the
// members of a given group together in contiguous rows. If this
// is not the case, this code will fail miserably.
Hashtable members = new Hashtable();
boolean more = true;
// We expect our ResultSet to already point at the first member of
// a group, hence this being a "do ... while" loop.
do
{
String groupName = rs.getString(1);
String memberName = rs.getString(2);
if (name == null)
{
name = groupName;
}
else if (groupName.equals(name) == false)
{
// We've encountered a group with a different name than the
// one we were interested in, so we're done for the current
// invocation.
break;
}
Principal p = getPrincipal(memberName);
if (p == null)
{
throw new RDBMSException("group \"" + name + "\" contains nonexistent " +
"principal \"" + memberName + "\"");
}
members.put(memberName, p);
} while (more = rs.next());
RDBMSGroup result = realm.createGroup(name, members);
// Sanity-check the new group to ensure that it doesn't contain
// any groups that contain it. You can turn this off if your
// database can't get corrupted in this way.
if (true)
{
Enumeration enum = members.elements();
while (enum.hasMoreElements())
{
Object obj = enum.nextElement();
if (obj instanceof Group)
{
Group g = (Group) obj;
if (g.isMember(result))
{
throw new RDBMSException("group membership circularity between \"" +
g.getName() + "\" and \"" + name + "\"");
}
}
}
}
if (more == false)
{
// We've hit the end of the ResultSet, so let our caller know.
throw new Finished(result);
}
// We have not hit the end of the ResultSet, so just return
// normally.
return result;
}
public boolean addGroupMember(RDBMSGroup group, Principal member)
throws SQLException
{
addGroupMemberStmt.setString(1, group.getName());
addGroupMemberStmt.setString(2, member.getName());
int rows = addGroupMemberStmt.executeUpdate();
if (rows != 1)
{
throw new RDBMSException("insert updated " + rows + " rows (should be 1)");
}
return true;
}
public boolean removeGroupMember(RDBMSGroup group, Principal member)
throws SQLException
{
removeGroupMemberStmt.setString(1, group.getName());
removeGroupMemberStmt.setString(2, member.getName());
int rows = removeGroupMemberStmt.executeUpdate();
if (rows != 1)
{
throw new RDBMSException("delete updated " + rows + " rows (should be 1)");
}
return true;
}
/**
* Get an ACL from the database, or null if the ACL doesn't exist.
*/
public Acl getAcl(String name)
throws SQLException
{
if (realm.log != null)
realm.log.debug("getAcl(\"" + name + "\")");
getAclEntriesStmt.setString(1, name);
ResultSet rs = getAclEntriesStmt.executeQuery();
try
{
return rs.next() ? getAclInternal(name, rs) : null;
}
catch (Finished f)
{
return (Acl) f.getValue();
}
finally
{
rs.close();
}
}
/**
* Get all ACLs from the database.
*/
public Enumeration getAcls()
throws SQLException
{
if (realm.log != null)
realm.log.debug("getAcls()");
ResultSet rs = getAclsStmt.executeQuery();
Vector acls = new Vector(1);
try
{
if (rs.next())
{
while (true)
{
acls.addElement(getAclInternal(null, rs));
}
}
}
catch (Finished f)
{
acls.addElement(f.getValue());
}
finally
{
rs.close();
}
return acls.elements();
}
/**
* Called by both getAcl and getAcls.
*/
protected Acl getAclInternal(String name, ResultSet rs)
throws Finished, SQLException
{
// This method follows a similar pattern to getGroupInternal, but
// has the added twist that it expects rows for a given ACL to be
// grouped together by principal.
boolean more = true;
AclImpl result = new AclImpl(aclOwner, null);
AclEntryImpl entry = null;
String currentPrincipal = null;
try
{
do
{
String aclName = rs.getString(1);
String principal = rs.getString(2);
String permission = rs.getString(3);
if (name == null)
{
name = aclName;
}
else if (aclName.equals(name) == false)
{
break;
}
if (currentPrincipal == null || currentPrincipal.equals(principal) == false)
{
// We're dealing with a new principal, so create a new AclEntry.
currentPrincipal = principal;
// There's an ordering constraint imposed here by the
// AclImpl implementation: we must add all the Permissions
// we will need to an AclEntry before adding the AclEntry to
// the Acl.
if (entry != null)
{
result.addEntry(aclOwner, entry);
}
Principal p = getPrincipal(principal);
if (p == null)
{
throw new RDBMSException("acl \"" + name + "\" contains nonexistent " +
"principal \"" + principal + "\"");
}
entry = new AclEntryImpl(p);
}
entry.addPermission(new PermissionImpl(permission));
} while (more = rs.next());
if (entry != null)
{
result.addEntry(aclOwner, entry);
}
result.setName(aclOwner, name);
}
catch (NotOwnerException e)
{
throw new RDBMSException("caught unexpected exception", e);
}
if (more == false)
{
throw new Finished(result);
}
return result;
}
/**
* Resolve a name to a User or Group. If the principal in question
* doesn't exist in the database, we return null.
*/
public Principal getPrincipal(String name)
throws SQLException
{
Principal result = getUser(name);
if (result == null)
{
result = getGroup(name);
}
return result;
}
/**
* Obtain the named permission from the database, or null if none.
*/
public Permission getPermission(String name)
throws SQLException
{
if (realm.log != null)
realm.log.debug("getPermission(\"" + name + "\")");
getPermissionStmt.setString(1, name);
ResultSet rs = getPermissionStmt.executeQuery();
try
{
return rs.next() ? new PermissionImpl(rs.getString(1)) : null;
}
finally
{
rs.close();
}
}
/**
* Return an Enumeration of the permissions for this realm.
*/
public Enumeration getPermissions()
throws SQLException
{
if (realm.log != null)
realm.log.debug("getPermissions()");
ResultSet rs = getPermissionsStmt.executeQuery();
Vector permissions = new Vector(1);
try
{
while (rs.next())
{
permissions.addElement(new PermissionImpl(rs.getString(1)));
}
return permissions.elements();
}
finally
{
rs.close();
}
}
/**
* Clean up after ourselves.
*/
protected void finalize()
{
close();
}
/**
* Clean up after ourselves.
*/
public void close()
{
try
{
try
{
if (conn != null)
conn.close();
}
catch (SQLException e)
{
// ignore
}
try
{
if (conn2 != null)
conn2.close();
}
catch (SQLException e)
{
// ignore
}
try
{
if (conn3 != null)
conn3.close();
}
catch (SQLException e)
{
// ignore
}
try
{
if (conn4 != null)
conn4.close();
}
catch (SQLException e)
{
// ignore
}
}
finally
{
realm = null;
conn = null;
conn2 = null;
conn3 = null;
conn4 = null;
getUserStmt = null;
getGroupMembersStmt = null;
getPermissionStmt = null;
getAclEntriesStmt = null;
getUsersStmt = null;
getGroupsStmt = null;
getAclsStmt = null;
getPermissionsStmt = null;
}
}
/**
* This is the factory class that creates instances of the
* RDBMSDelegate class for pooling.
*/
static class DFactory implements Factory
{
/**
* The realm that owns all delegates created by this factory.
*/
private RDBMSRealm owner;
/**
* Create an instance of the factory, owned by the given realm.
*/
DFactory(RDBMSRealm owner)
{
this.owner = owner;
}
/**
* Create a new delegate.
*/
public Object newInstance()
throws InvocationTargetException
{
if (owner.log != null)
owner.log.debug("new instance");
return new RDBMSDelegate(owner);
}
/**
* Destroy a delegate.
*/
public void destroyInstance(Object obj)
{
if (owner.log != null)
owner.log.debug("destroy instance");
((RDBMSDelegate) obj).close();
}
}
}