You are viewing a plain text version of this content. The canonical link for it is here.
Posted to slide-dev@jakarta.apache.org by oz...@apache.org on 2004/06/22 19:56:41 UTC
cvs commit: jakarta-slide/src/stores/org/apache/slide/store/txjndi JNDIPrincipalStore.java
ozeigermann 2004/06/22 10:56:41
Added: src/stores/org/apache/slide/store/txjndi
JNDIPrincipalStore.java
Log:
Added JNDI pricipal store kindly donated by James Mason.
Revision Changes Path
1.1 jakarta-slide/src/stores/org/apache/slide/store/txjndi/JNDIPrincipalStore.java
Index: JNDIPrincipalStore.java
===================================================================
/*
* $Header: /home/cvs/jakarta-slide/src/stores/org/apache/slide/store/txjndi/JNDIPrincipalStore.java,v 1.1 2004/06/22 17:56:41 ozeigermann Exp $
* $Revision: 1.1 $
* $Date: 2004/06/22 17:56:41 $
*
* ====================================================================
*
* Copyright 1999-2002 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
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();
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: slide-dev-unsubscribe@jakarta.apache.org
For additional commands, e-mail: slide-dev-help@jakarta.apache.org