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