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