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:[EMAIL PROTECTED]">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 [EMAIL PROTECTED] --------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]
