You are viewing a plain text version of this content. The canonical link for it is here.
Posted to slide-user@jakarta.apache.org by James Mason <Ma...@ah.org> on 2004/05/29 00:49:31 UTC
LDAP Store for Slide
I'd really like to thank everyone for the feedback, it's been really helpful. Here's the first version of the code. It works, but has a few issues. See the class comments for details. I'm going to be gone for the next week but if anyone has time to test this and suggest/make improvements I'd really appreciate it. I've tested this with Sun JDK 1.4.1_01 on Win2k with the Slide 2.0/Tomcat 5 bundle and Novell's eDirectory 8.6.2.
Below is the java code and a sample Domain.xml.
== Java ===============================
package org.apache.slide.store.txjndi;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import org.apache.slide.common.AbstractXAService;
import org.apache.slide.common.NamespaceAccessToken;
import org.apache.slide.common.ServiceAccessException;
import org.apache.slide.common.ServiceConnectionFailedException;
import org.apache.slide.common.ServiceDisconnectionFailedException;
import org.apache.slide.common.ServiceInitializationFailedException;
import org.apache.slide.common.ServiceParameterErrorException;
import org.apache.slide.common.ServiceParameterMissingException;
import org.apache.slide.common.ServiceResetFailedException;
import org.apache.slide.common.Uri;
import org.apache.slide.content.NodeProperty;
import org.apache.slide.content.NodeRevisionContent;
import org.apache.slide.content.NodeRevisionDescriptor;
import org.apache.slide.content.NodeRevisionDescriptors;
import org.apache.slide.content.NodeRevisionNumber;
import org.apache.slide.content.RevisionAlreadyExistException;
import org.apache.slide.content.RevisionDescriptorNotFoundException;
import org.apache.slide.content.RevisionNotFoundException;
import org.apache.slide.store.ContentStore;
import org.apache.slide.store.NodeStore;
import org.apache.slide.store.RevisionDescriptorStore;
import org.apache.slide.store.RevisionDescriptorsStore;
import org.apache.slide.structure.ObjectAlreadyExistsException;
import org.apache.slide.structure.ObjectNode;
import org.apache.slide.structure.ObjectNotFoundException;
import org.apache.slide.structure.SubjectNode;
import org.apache.slide.util.logger.Logger;
/**
* <p>
* This is a read-only Store implementation for retrieving Slide users
* and roles from an LDAP server. It has been tested with Slide 2.0 (the
* Tomcat 5 binary bundle) and Novell's eDirectory 8.6.2. It is very slow
* (caching helps but renders the Store almost useless for production) and
* still needs a lot of work. It currently implements ContentStore,
* NodeStore, RevisionDescriptorStore and RevisionDescriptorsStore.
* Another Store implementation must be used for SecurityStore and
* LockStore since there is no way to get reasonable values for this data
* from LDAP.
* </p>
* <h3>Prerequisites</h3>
* <p>
* To use this Store your app server must be setup to authenticate
* users using the LDAP server. For Tomcat 5 @see
* http://jakarta.apache.org/tomcat/tomcat-5.0-doc/realm-howto.html#JNDIRealm.
* You cannot use the Slide Realm to authenticate users because this Store
* does not expose a <span style="font-style: italic;">password</span>
* property.
* </p>
* <h3>Store Parameters</h3>
* <p>
* Parameters used in Domain.xml when setting up the Store.
* </p>
* <dl>
* <dt>jndi.container</dt>
* <dd>The base LDAP context you wish to search. Example: <i>ou=Users,o=AdventistHealth</i></dd>
* </dl>
* <dl>
* <dt>jndi.attributes.rdn</dt>
* <dd>The attribute used to uniquely identify the objects you're fetching. Usually uid or cn.</dd>
* </dl>
* <dl>
* <dt>jndi.search.filter</dt>
* <dd>The filter string to use for the search. Example: <i>(objectClass=inetOrgPerson)</i>. @see http://java.sun.com/j2se/1.4.2/docs/api/javax/naming/directory/DirContext.html#search(javax.naming.Name,%20java.lang.String,%20javax.naming.directory.SearchControls)</dd>
* </dl>
* <dl>
* <dt>jndi.search.scope</dt>
* <dd>The Scope of the search. Can be one of <i>OBJECT_SCOPE</i>, <i>ONELEVEL_SCOPE</i>, <i>SUBTREE_SCOPE</i>. @see
* http://java.sun.com/j2se/1.4.2/docs/api/javax/naming/directory/SearchControls.html#OBJECT_SCOPE</dd>
* </dl>
* <dl>
* <dt>jndi.search.attributes</dt>
* <dd>A comma delimited list of the attributes you want returned with your search results. Example: <i>givenName, uid, mail</i></dd>
* </dl>
* <dl>
* <dt>java.naming.*</dt>
* <dd>Parameters for connecting to the LDAP server. @see http://java.sun.com/j2se/1.4.2/docs/api/javax/naming/InitialContext.html</dd>
* </dl>
* <h3>TODO:</h3>
* <ol>
* <li>
* I'd like to see this implemented as a ResourceManager rather than
* a stand-alone Store. I think it would fit into Slide's framework better
* that way and mean less duplicated code.
* </li>
* <li>
* Performance needs help. It takes about 15 seconds to retrieve
* 20,000 objects from an LDAP server, but it take far longer than that to
* get an uncached listing from the /users directory. If performance can't
* be improved then a caching solution needs to be found that will allow
* the cache to be refreshed (either periodically or event based).
* </li>
* <li>
* I think there's still room for a full-fledged LDAP store. The way
* LDAP exposes a directory as a graph-of-objects-with-properties and
* Slide exposes a repository as a graph-of-objects-with-properties seems
* very similar to me ;). However, adapting the structure of most LDAP
* servers to the user/role structure that Slide uses would be a bit of a
* pain, so I don't think this kind of Store would be useful for
* users/roles in Slide. I have heard of people using LDAP to keep track
* of server inventories and things like that, though, and I think it
* would work well there.
* </li>
* </ol>
*
* @author <a href="mailto:masonjm@ah.org">James Mason</a>
*/
public class JNDIPrincipalStore
extends AbstractXAService
implements ContentStore, NodeStore, RevisionDescriptorStore,
RevisionDescriptorsStore {
public static final String JNDI_PROPERTY_PREFIX = "java.naming";
public static final String JNDI_CONTAINER_PARAMETER = "jndi.container";
public static final String JNDI_FILTER_PARAMETER = "jndi.search.filter";
public static final String JNDI_RDN_ATTRIBUTE_PARAMETER = "jndi.attributes.rdn";
public static final String JNDI_GROUPMEMBERSET_PARAMETER = "jndi.attributes.groupmemberset";
public static final String JNDI_SEARCH_ATTRIBUTES_PARAMETER = "jndi.search.attributes";
public static final String JNDI_SEARCH_SCOPE_PARAMETER = "jndi.search.scope";
public static final String LDAP_NAMESPACE = "LDAP:";
public static final String LOG_CHANNEL = JNDIPrincipalStore.class.getName();
// TODO - figure out how to get this from the Domain.
public static final String USERS_SCOPE = "/users";
protected Hashtable ctxParameters;
protected DirContext ctx;
protected boolean isConnected = false;
protected String container;
protected String filter;
protected String rdnAttribute;
protected int searchScope;
protected String[] descriptorAttributes;
protected String groupMemberSet;
public JNDIPrincipalStore() {
ctxParameters = new Hashtable();
}
// ----------------------------------------------------------- Service Methods --------
public void initialize( NamespaceAccessToken token )
throws ServiceInitializationFailedException {
super.initialize( token );
}
public void setParameters( Hashtable parameters )
throws ServiceParameterErrorException,
ServiceParameterMissingException {
Iterator keys = parameters.keySet().iterator();
while ( keys.hasNext() ) {
String key = (String)keys.next();
if ( key.startsWith( JNDI_PROPERTY_PREFIX ) ) {
ctxParameters.put( key, parameters.get( key ) );
}
}
container = (String)parameters.get( JNDI_CONTAINER_PARAMETER );
filter = (String)parameters.get( JNDI_FILTER_PARAMETER );
rdnAttribute = (String)parameters.get( JNDI_RDN_ATTRIBUTE_PARAMETER );
String ss = (String)parameters.get( JNDI_SEARCH_SCOPE_PARAMETER );
if ( ss.equals( "OBJECT_SCOPE" ) ) {
searchScope = SearchControls.OBJECT_SCOPE;
} else if ( ss.equals( "ONELEVEL_SCOPE" ) ) {
searchScope = SearchControls.ONELEVEL_SCOPE;
} else if ( ss.equals( "SUBTREE_SCOPE" ) ) {
searchScope = SearchControls.SUBTREE_SCOPE;
}
String searchAttributesString = (String)parameters.get(
JNDI_SEARCH_ATTRIBUTES_PARAMETER );
ArrayList searchAttributesList = new ArrayList();
StringTokenizer tok = new StringTokenizer( searchAttributesString, "," );
while ( tok.hasMoreTokens() ) {
searchAttributesList.add( tok.nextToken().trim() );
}
String gms = (String)parameters.get( JNDI_GROUPMEMBERSET_PARAMETER );
if ( gms != null ) {
searchAttributesList.add( gms );
groupMemberSet = gms;
} else {
groupMemberSet = "";
}
descriptorAttributes = (String[])searchAttributesList.toArray( new String[0] );
}
public boolean cacheResults() {
// TODO - Either make jndi lookups faster or fix caching
// There is currently no way (that I know of) to clear the cache. Since all
// of the information this store displays is managed externally to Slide
// there needs to be a way to tell Slide to update the cached objects.
// This means either extending Slide's caching or writing a new implementation
// just for this store.
return true;
}
//------------------------------------------------------ NodeStore Methods ----------
public void storeObject( Uri uri, ObjectNode object ) throws ServiceAccessException, ObjectNotFoundException {}
public void createObject( Uri uri, ObjectNode object ) throws ServiceAccessException, ObjectAlreadyExistsException {}
public void removeObject( Uri uri, ObjectNode object ) throws ServiceAccessException, ObjectNotFoundException {}
public ObjectNode retrieveObject( Uri uri ) throws ServiceAccessException,
ObjectNotFoundException {
getLogger().log( "Calling retrieveObject(" + uri.toString() + ").",
LOG_CHANNEL, Logger.DEBUG );
Uri parentUri = uri.getParentUri();
String objectName = getObjectNameFromUri( uri );
SubjectNode node = new SubjectNode( uri.toString() );
// As long as this node isn't the root node create a parent binding.
// This doesn't appear to make any difference, but just in case.
if ( !uri.toString().equals( "/" ) ) {
SubjectNode parentNode = new SubjectNode( parentUri.toString() );
node.addParentBinding( objectName, parentNode );
}
// If the uri matches the scope create a SubjectNode with bindings for all
// of the results from a jndi search
if ( uri.isStoreRoot() ) {
SearchControls controls = new SearchControls();
controls.setSearchScope( searchScope );
try {
NamingEnumeration results = ctx.search(
container,
filter,
controls );
if ( !results.hasMore() ) {
getLogger().log(
"No objects found in container " + container +
" that match filter " + filter + ".",
LOG_CHANNEL,
Logger.WARNING );
}
while ( results.hasMore() ) {
SearchResult result = null;
try {
result = (SearchResult)results.next();
} catch ( NamingException e ) {
getLogger().log(
"Error getting next search result.",
e, LOG_CHANNEL, Logger.ERROR );
}
String name = result.getName();
String value = parseLdapName( name );
SubjectNode childNode = new SubjectNode(
uri.toString() + "/" + value );
node.addBinding( value, childNode );
}
} catch ( NamingException e ) {
getLogger().log(
"Error during search.",
e, LOG_CHANNEL, Logger.ERROR );
}
} else {
// If the uri matches the scope + something else try to do a lookup
// of the "+ something" in LDAP.
try {
if ( ctx.lookup(
rdnAttribute + "=" + objectName + "," +
container ) == null ) {
throw new ObjectNotFoundException( uri );
}
} catch ( NamingException e ) {
getLogger().log(
"Error retrieving " + uri.toString(),
e, LOG_CHANNEL, Logger.ERROR );
throw new ServiceAccessException( this, e );
}
}
return node;
}
//-------------------------------------------- RevisionDescriptorStore Methods --------
public void createRevisionDescriptor( Uri uri, NodeRevisionDescriptor revisionDescriptor ) throws ServiceAccessException {}
public void storeRevisionDescriptor( Uri uri, NodeRevisionDescriptor revisionDescriptor ) throws ServiceAccessException, RevisionDescriptorNotFoundException {}
public void removeRevisionDescriptor( Uri uri, NodeRevisionNumber revisionNumber ) throws ServiceAccessException {}
public NodeRevisionDescriptor retrieveRevisionDescriptor( Uri uri,
NodeRevisionNumber revisionNumber ) throws ServiceAccessException,
RevisionDescriptorNotFoundException {
getLogger().log( "Calling retrieveRevisionDescriptor(" + uri.toString() + ").",
LOG_CHANNEL, Logger.DEBUG );
String objectName = getObjectNameFromUri( uri );
Hashtable props = new Hashtable();
String resourceType = "<collection/>";
if ( !uri.isStoreRoot() ) {
resourceType += "<principal/>";
}
props.put(
"DAV:resourcetype",
new NodeProperty( "resourcetype", resourceType, "DAV:", "", false ) );
props.put(
"DAV:displayname",
new NodeProperty( "displayname", objectName, "DAV:", "", false ) );
// The storeRoot isn't a real object so it doesn't have any parameters to look up
if ( !uri.isStoreRoot() ) {
String localFilter = rdnAttribute + "=" + objectName;
SearchControls controls = new SearchControls();
controls.setSearchScope( searchScope );
controls.setReturningAttributes( descriptorAttributes );
try {
NamingEnumeration results = ctx.search(
container,
localFilter,
controls );
if ( !results.hasMore() ) {
throw new RevisionDescriptorNotFoundException( uri.toString() );
}
while ( results.hasMore() ) {
SearchResult result = null;
try {
result = (SearchResult)results.next();
} catch ( NamingException e ) {
getLogger().log(
"Error getting search result with filter: " + localFilter +
" from container: " + container + ".",
LOG_CHANNEL, Logger.ERROR );
throw new ServiceAccessException( this, e );
}
NamingEnumeration attributes = result.getAttributes().getAll();
while ( attributes.hasMore() ) {
Attribute attribute = (Attribute)attributes.next();
StringBuffer valueString = new StringBuffer();
boolean isGms = attribute.getID().equals( groupMemberSet );
boolean isMva = attribute.size() > 1;
for ( int i = 0; i < attribute.size(); i++ ) {
try {
Object value = attribute.get( i );
if ( !( value instanceof String ) ) {
getLogger().log(
"Non-string value found for " + attribute.getID() +
".",
LOG_CHANNEL,
Logger.DEBUG );
continue;
}
if ( isGms ) {
valueString.append( "<D:href xmlns:D='DAV:'>" );
valueString.append( USERS_SCOPE ).append( "/" );
valueString.append( parseLdapName( value.toString() ) );
valueString.append( "</D:href>" );
} else {
if ( isMva ) {
valueString.append( "<mva xmlns=\"" )
.append( LDAP_NAMESPACE ).append( "\">" );
valueString.append( value.toString().toLowerCase() );
valueString.append( "</mva>" );
} else {
valueString.append( value.toString().toLowerCase() );
}
}
} catch ( NamingException e ) {
getLogger().log(
"Error fetching next attribute value for attribute " +
attribute.getID() + ".",
e, LOG_CHANNEL, Logger.DEBUG );
}
}
if ( isGms ) {
props.put(
"DAV:group-member-set",
new NodeProperty(
"group-member-set",
valueString.toString(),
"DAV:" ) );
} else {
props.put(
LDAP_NAMESPACE + attribute.getID(),
new NodeProperty(
attribute.getID(),
valueString.toString(),
LDAP_NAMESPACE ) );
}
}
}
} catch ( NamingException e ) {
getLogger().log(
"Error during search.",
e, LOG_CHANNEL, Logger.ERROR );
}
}
return new NodeRevisionDescriptor(
new NodeRevisionNumber( 1, 0 ),
"main",
new Vector(),
props );
}
// --------------------------------------------- RevisionDescriptorsStore Methods -----
public NodeRevisionDescriptors retrieveRevisionDescriptors( Uri uri ) throws ServiceAccessException, RevisionDescriptorNotFoundException {
getLogger().log( "Calling retrieveRevisionDescriptors(" + uri.toString() + ").",
LOG_CHANNEL, Logger.INFO );
NodeRevisionNumber rev = new NodeRevisionNumber( 1, 0 );
Hashtable workingRevisions = new Hashtable();
workingRevisions.put( "1.0", rev );
Hashtable latestRevisionNumbers = new Hashtable();
latestRevisionNumbers.put( "1.0", rev );
// From looking at NodeRevisionDescriptors.cloneObject I see branchNames is
// supposed to store Vector. I'm guessing the Vector is of revision numbers
Vector branches = new Vector();
branches.add( rev );
Hashtable branchNames = new Hashtable();
branchNames.put( "main", branches );
return new NodeRevisionDescriptors(
uri.toString(),
rev,
workingRevisions,
latestRevisionNumbers,
branchNames,
false );
}
public void createRevisionDescriptors( Uri uri, NodeRevisionDescriptors revisionDescriptors ) throws ServiceAccessException {}
public void storeRevisionDescriptors( Uri uri, NodeRevisionDescriptors revisionDescriptors ) throws ServiceAccessException, RevisionDescriptorNotFoundException {}
public void removeRevisionDescriptors( Uri uri ) throws ServiceAccessException {}
// --------------------------------------------------------- XA Methods --------------
public void connect() throws ServiceConnectionFailedException {
try {
ctx = new InitialDirContext( ctxParameters );
isConnected = true;
} catch ( NamingException e ) {
getLogger().log("Error Connecting to LDAP Server",
e,
LOG_CHANNEL,
Logger.EMERGENCY );
throw new ServiceConnectionFailedException( this, e );
}
}
public void disconnect() throws ServiceDisconnectionFailedException {
try {
ctx.close();
} catch ( NamingException e ) {
getLogger().log( "Error disconnecting from LDAP",
e, LOG_CHANNEL, Logger.WARNING );
ctx = null;
} finally {
isConnected = false;
}
}
public void reset() throws ServiceResetFailedException {}
public boolean isConnected() throws ServiceAccessException {
return isConnected;
}
public int getTransactionTimeout() throws XAException {
return 0;
}
public boolean setTransactionTimeout( int seconds ) throws XAException {
return false;
}
public boolean isSameRM( XAResource rm ) throws XAException {
return false;
}
public Xid[] recover( int flag ) throws XAException {
return new Xid[0];
}
public int prepare( Xid txId ) throws XAException {
return XA_RDONLY;
}
public void forget( Xid txId ) throws XAException {}
public void rollback( Xid txId ) throws XAException {}
public void end( Xid txId, int flags ) throws XAException {}
public void start( Xid txId, int flags ) throws XAException {}
public void commit( Xid txId, boolean onePhase ) throws XAException {}
// -------------------------------------------------- ContentStore Methods ----------
public NodeRevisionContent retrieveRevisionContent( Uri uri, NodeRevisionDescriptor revisionDescriptor ) throws ServiceAccessException, RevisionNotFoundException {
return new NodeRevisionContent();
}
public void createRevisionContent( Uri uri, NodeRevisionDescriptor revisionDescriptor, NodeRevisionContent revisionContent ) throws ServiceAccessException, RevisionAlreadyExistException {}
public void storeRevisionContent( Uri uri, NodeRevisionDescriptor revisionDescriptor, NodeRevisionContent revisionContent ) throws ServiceAccessException, RevisionNotFoundException {}
public void removeRevisionContent( Uri uri, NodeRevisionDescriptor revisionDescriptor ) throws ServiceAccessException {}
// --------------------------------------------------- Helper Methods ---------------
protected String parseLdapName( String name ) {
// Since attribute values can contain pretty much anything, parsing
// name to get the attribute value isn't terribly accurate.
// The slow way is to find a value for the attribute that matches the
// results from getName(), but that is horribly horribly slow.
// On the assumption that "," is more likely to be in the value than
// "=", this should work most of the time and be faster... I hope.
int firstEqual = name.indexOf( "=" );
if ( firstEqual < 0 ) {
firstEqual = 0;
}
int secondEqual = name.substring( firstEqual + 1 ).indexOf( "=" );
if ( secondEqual < 0 ) {
secondEqual = name.length() - 1;
} else {
secondEqual = secondEqual + firstEqual + 1;
}
int end = name.substring( 0, secondEqual ).lastIndexOf( "," );
if ( end < 0 ) {
end = name.length();
}
String value = name.substring( firstEqual + 1, end ).toLowerCase();
return value;
}
protected String getObjectNameFromUri( Uri uri ) {
String objectName = uri.toString().substring(
uri.toString().lastIndexOf( "/" ) + 1 );
return objectName.toLowerCase();
}
}
== Domain.xml ==================================
� my email client is being picky. I'll post this in a follow up email �
James Mason
Adventist Health
Programmer/Analyst
916.783.2576
masonjm@ah.org
---------------------------------------------------------------------
To unsubscribe, e-mail: slide-user-unsubscribe@jakarta.apache.org
For additional commands, e-mail: slide-user-help@jakarta.apache.org