18 Customizing Oracle Virtual Directory

This chapter explains how to customize Oracle Virtual Directory and contains the following sections:

18.1 Setting Localized Languages for Oracle Directory Services Manager

Oracle Virtual Directory includes localized translations for the Oracle Directory Services Manager interface in the following languages:

  • French

  • Italian

  • German

  • Spanish

  • Brazilian Portuguese

  • Japanese

  • Traditional Chinese

  • Simplified Chinese

  • Korean

You can set the language for the Oracle Directory Services Manager interface using your web browser's language settings. Refer to your web browser's documentation for specific information on setting languages.

Notes:

Only users who have Oracle Directory Services Manager Administrator access (usually cn=orcladmin) can log in to Oracle Directory Services Manager.

18.2 Creating and Configuring Custom Adapters

Oracle Virtual Directory supports the ability to create custom adapters using plug-ins that can connect to almost any data source with a defined API. For example, you can use custom adapters to abstract information available through web services. A custom adapter is an adapter that has no functionality itself—it is a place holder where adapter level plug-ins can be configured to implement its functions instead. By default, Custom Adapters do not map to any data source. Plug-ins, such as the Diameter plug-in, that are added to Custom Adapters on the Plug-In tab in Oracle Directory Services Manager provide data to Custom Adapters. Typically, Custom Adapters are written by customers that must connect Oracle Virtual Directory to non-LDAP or non-database services, such as Web Services.

This section contains the following topics:

18.2.1 Creating Custom Adapters

Perform the following steps to create a Custom Adapter using Oracle Directory Services Manager:

  1. Log in to Oracle Directory Services Manager.

  2. Select Adapter from the task selection bar. The Adapter navigation tree appears.

  3. Click the Create Adapter button. The New Adapter Wizard appears.

  4. Perform the following steps to define the Type of adapter:

    1. Select Custom from the Adapter Type list.

    2. Enter a unique name for the Custom Adapter in the Adapter Name field. The adapter name value is used in other configuration fields that must reference the adapter.

    3. Select the Default template from the Adapter Template list.

    4. Click Next. The Settings screen appears.

  5. Enter a valid base DN (in DN format) in the Adapter Suffix/Namespace field. This field defines the root DN for which the adapter provides information. The DN defined, and the child entries below it, comprise the adapter's namespace. Enter a value in the Adapter Suffix field that should be the base DN value for returned entries. For example, if you enter dc=mydomain,dc=com in the Adapter Suffix/Namespace field, all entries end with dc=mydomain,dc=com.

  6. Click Next. A summary of the Custom Adapter settings appears. Review the settings and click Finish to create the Custom Adapter. The Custom Adapter appears in the Adapter tree.

After you create the Custom Adapter you can configure it using the procedures in Configuring Custom Adapters.

18.2.2 Configuring Custom Adapters

This section describes how to configure Custom Adapter settings, including:

18.2.2.1 Configuring Custom Adapter General Settings

After you create the Custom Adapter you can configure the general settings for the adapter by clicking the adapter name in the Adapter tree, clicking the General tab, setting values for the following fields, and clicking Apply:

Root

This field defines the root DN that the adapter provides information for. The DN defined, and the child entries below it, comprise the adapter's namespace. The value you enter in this field should be the base DN value for returned entries. For example, if you enter dc=mydomain,dc=com in the field, all entries end with dc=mydomain,dc=com.

Active

An adapter can be configured as active (enabled) or inactive (disabled). An adapter configured as inactive does not start during a server restart or an attempted adapter start. Use the inactive setting to keep old configurations available or in stand-by without having to delete them from the configuration. The default setting is active.

18.2.2.2 Configuring Adapter Routing

After you create the adapter you can configure routing for the adapter by clicking the adapter name in the Adapter tree, clicking the Routing tab, and referring to "Understanding Routing Settings".

Note:

Enable the Bind Support routing setting when defining Custom Adapters that may or may not support a bind operation.

18.2.2.3 Configuring Adapter Plug-ins and Mappings

After you create the adapter you can apply Plug-ins and Mappings to the adapter by clicking the adapter name in the Adapter tree, clicking the Plug-Ins tab, and referring to "Managing Adapter Plug-ins" and "Applying Mappings to Adapters".

18.3 Developing Custom Java Plug-Ins

This section explains how to develop custom Java plug-ins for Oracle Virtual Directory and contains the following section:

18.3.1 Overview

Oracle Virtual Directory enables you to create and deploy custom Java plug-ins that can process and manipulate LDAP operations as they pass through the Oracle Virtual Directory. Plug-ins can be positioned at either a global level, where they see and affect all requests, or at an adapter level, where they see and affect only requests for a particular adapter. You can also create and deploy plug-ins to run on particular operations and for certain namespaces.

Note:

If you rename attributes using custom Java plug-ins, Oracle Virtual Directory supports search on the renamed attribute/value only if the custom code overrides the incoming filter object, as is in the DB_Groups Mapping.

Each Oracle Virtual Directory plug-in has a specific implementation point, as listed in Table 18-1:

Table 18-1 Plug-In Implementation Points

Implementation Point Description

Configuration

Plug-in configuration data. The custom portion of the configuration consists of name and value pairs of initialization parameters

Startup / Shutdown

The init(PluginInit initParams, String name) and destroy() methods are called on plug-in initialization and de-initialization.

Availability

The available(Chain chain, DirectoryString base) method is called before execution of the plug-in to determine if the plug-in will be executed.

Operations

The various operational methods to be called.


This chapter demonstrates how to create a custom plug-in by explaining the implementation points listed in Table 18-1. The chapter provides information for a fictitious example plug-in called the Bad Password Count plug-in which would detect if a bind operation has failed or succeeded. If the operation succeeded, then the count would be cleared and if the bind fails, then the count would increase. The fictitious Bad Password Count plug-in also ensures that the bad password count cannot be changed from outside the directory.

Note:

The Bad Password Count plug-in described in this chapter is a fictitious example used to demonstrate how Oracle Virtual Directory plug-ins and its chain system operate. Oracle Virtual Directory does not include a Bad Password Count plug-in, though it could support one if you created it.

18.3.2 Understanding the Chain System

Oracle Virtual Directory plug-ins follow an implementation based on the Java Servlet 2.3 Filter model where a single method is used to handle the pre-operation and post-operation, and to determine if an operation should continue. Multiple plug-ins are combined to form a chain of plug-ins. To demonstrate this chain implementation, consider the following situation where the fictitious example Bad Password Count plug-in determines if the bad password count attribute should be added to an entry being added to the directory.

You have the ability to manipulate the request when the add method is called, which enables you to manipulate the passed-in attributes and their values (for example, to change objectclass value inetOrgPerson to user if you were masking ActiveDirectory as a standard LDAP directory) or to handle the storage of the data into your non-directory or database system (such as in a custom adapter). To allow the virtual directory to have a chance to further process the request through other plug-ins, you would call the chain.nextAdd method. Most plug-in methods have corresponding chain.next<XXX> methods. If you do not want to allow further processing of the request by plug-ins, you can omit the chain.next<XXX> call.

Note:

The adapter type is irrelevant to the plug-ins, as plug-ins can be added to any type of adapter.

18.3.3 Plug-In Implementation Points

Before you can build a custom plug-in, you must decide whether to implement the com.octetstring.vde.chain.Plugin interface or extend the com.octetstring.vde.chain.BasePlugin class. The BasePlugin class is a convenience that allows a plug-in developer to only implement the methods for operations to be handled by the plug-in. The example plug-in provided in this chapter extends the BasePlugin class to simplify the implementation.

The sections in this section describe the Oracle Virtual Directory plug-in implementation points, including:

18.3.3.1 Configuration, Startup, and Shutdown Plug-In Implementation Points

Configuration is the first plug-in implementation point. Plug-ins are configured using a set of simple name and value pairs provided by the Oracle Virtual Directory configuration system. The pairs are provided to the plug-in developer through the params argument to the init method of the plug-in. The example plug-in provided in this chapter includes the following configuration options:

  • countAttribute: An attribute to be attached to all user entries that store the bad password count.

  • addOnCreate: Boolean value set to true if the plug-in adds this attribute when a user is created.

  • objectClassForAdd: The object classes that represent users to which the attribute is added.

  • ignoreOnModify: Boolean value, set to true if modify requests on the countAttribute should be ignored.

The configuration options listed above are picked-up at the life cycle methods, which is the second implementation point. The init method is called on the initialization of the plug-in at server startup and the destroy method is called when the plug-in is being shutdown. Example 18-1 shows an example init method:

Example 18-1 Example init Method

/**
 * Passes initialization information to the Plug-in
 * 
 * @param initParams
 *            Hashmap of key/value pairs specified in initial config
 * @param name
 *            The name specified in the config for this Plug-in
 */
public void init(PluginInit initParams, String name) throws ChainException {
       //the countAttribute parameter is required
       if (!initParams.containsKey(BadPasswordCount.CONFIG_COUNT_ATTRIBUTE)) {
            throw new ChainException(name + ": The "
               + BadPasswordCount.CONFIG_COUNT_ATTRIBUTE
               + " attribute is required");
       }
       this.countAttribute = new DirectoryString(initParams
                 .get(BadPasswordCount.CONFIG_COUNT_ATTRIBUTE));
       this.attribType = SchemaChecker.getInstance().getAttributeType(
            this.countAttribute);
       //determine if add on create
       this.addOnCreate = initParams
            .containsKey(BadPasswordCount.CONFIG_ADD_ON_CREATE)
            && initParams.get(BadPasswordCount.CONFIG_ADD_ON_CREATE)
            .equalsIgnoreCase("true");

       if (this.addOnCreate) {
                    && !initParams
                     .containsKey(BadPasswordCount.CONFIG_OBJECTCLASS_FOR_ADD)) {
             throw new ChainException(name
                   + ": When adding count attribute, the parameter "
                   + BadPasswordCount.CONFIG_OBJECTCLASS_FOR_ADD
                   + " is required");
             }

             String[] objectClasses = initParams
                    .getVals(BadPasswordCount.CONFIG_OBJECTCLASS_FOR_ADD);
             this.objectClasses = new HashSet();

             for (int i = 0, m = objectClasses.length; i < m; i++) {
                  this.objectClasses.add(new DirectoryString(objectClasses[i]));

             }
       } else {
               this.addOnCreate = false;
       }

       logger.info("Adding on create : " + this.addOnCreate);
       //determine if the modify operation should be ignored
       this.ignoreModify = initParams
            .containsKey(BadPasswordCount.CONFIG_IGNORE_MODIFY)
            && initParams.get(BadPasswordCount.CONFIG_IGNORE_MODIFY)
            .equalsIgnoreCase("true");

The method in Example 18-1 checks the initialization parameters to setup the plug-in. If there is not enough configuration information, then the plug-in throws an exception, causing the plug-in to not be configured for operational use by the server. You are not required to implement the destroy method unless there is a need to release any connections or shutdown any services.

18.3.3.2 Availability Plug-In Implementation Point

The available implementation point follows the configuration and startup and shutdown implementation points. The available method is called before each plug-in can be called for a particular LDAP operation. If the available method returns as true, then the plug-in is executed. In Example 18-2, the available method checks for the existence of the ignoreOnModify option in the Request object. If it is defined, then the plug-in is skipped. Similarly, if the addonCreate option is set to false, the plug-in is skipped.

Example 18-2 Example Method Checking for ignoreOnModify Option

/**
 * Determines if a plugin is available for the current chain
 * 
 * @param chain
 * @param base
 * @return True or False if available for a particular chain & base
 */
public boolean available(Chain chain, DirectoryString base) {
 
       if (chain.getOperationType() == Chain.ADD_OP && !this.addOnCreate) {
             return false;
       } else if (chain.getOperationType() == Chain.MOD_OP
                  && this.ignoreOnModify) {
             return false;
       } else {
             return true;
       }
}

If the available method returns as true, the operation portion of the request will be executed.

18.3.3.3 Operation Plug-In Implementation Point

The final implementation point is operation implementations. Consider the following code implementation of a bind operation in Example 18-3:

Example 18-3 Example Bind Operation Implementation

/**
 * Moves through the "bind" operation's chain
 * 
 * @param chain
 *            The current chain
 * @param dn
 *            The DN for the user
 * @param password
 *            The user's password
 * @param result
 *            The result of the bind
 */
public void bind(Chain chain, Credentials creds, DirectoryString dn,
            BinarySyntax password, Bool result) throws DirectoryException,
            ChainException {
 
       // Pre-event processing 
 
// calls the next plug-in in the chain (or comment out if a handler)
      try {
         chain.nextBind(creds, dn, password, result);
      } catch (DirectoryException e) {
            throw e;
      }
 
      // Post-event processing
      if (result.booleanValue()) {
            // success, reset count
            setPasswordCount(chain, creds, dn, 0);
      } else {
            Vector searchAttributes = new Vector();
            searchAttributes.add(this.countAttribute);
 
            ChainVector results = new ChainVector();
            try
            {
            chain.getVSI().get(chain.getRequest(), creds, dn,
                new Int8((byte) 0), ParseFilter.parse("(objectClass=*)"),
                new Bool(false), searchAttributes, results);
 
            if (results.size() > 0) {
                 EntrySet es = (EntrySet) results.get(0);
                 Entry entry = es.getNext();
                 Vector values = entry.get(this.countAttribute);
                 Syntax value = (Syntax) values.get(0);
                 IntegerSyntax is = new IntegerSyntax(value.getValue());
                 setPasswordCount(chain, creds, dn,
                               ((int) is.getLongValue()) + 1);
            } else 
            {
                  setPasswordCount(chain, creds, dn, 1);
            chain.getVSI().get(...);            
            }
            }
            catch (Exception ex)
            {
 
            }
            finally
            {
                for (EntrySet entrySet : results)
                    entrySet.cancelEntrySet();
            }

            }
      }
}
 
private void setPasswordCount(Chain chain, Credentials creds,
             DirectoryString dn, int count) throws DirectoryException,
             ChainException {
 
      Vector values = new Vector();
      values.add(new IntegerSyntax(count));
      EntryChange modify = new EntryChange(EntryChange.MOD_REPLACE,
                  this.countAttribute, values);
      Vector changes = new Vector();
      changes.add(modify);
      chain.getVSI().modify(chain.getRequest(), creds, dn, changes);
 
}

The method in Example 18-3 shows an example where password failure counts are being maintained within the directory as a form of password policy. Notice that the method does not perform any pre-processing of the operation, nor does it attempt to take over the bind operation. The plug-ins bind method immediately calls the chain.nextBind method and waits for the bind to complete before moving forward with its own logic. Once the bind is complete, that is, control is returned from chain.nextBind, the plug-in checks to see if the bind was successful or not. If the bind was successful, the plug-in sets the failure count attribute to zero for the user. If the bind failed, then the current failure count is retrieved and an increased value is set.

The bind method uses the Virtual Services Interface (VSI) to modify records for the binding user. You can use the VSI interface throughout Oracle Virtual Directory as a consistent way to access directory information regardless of whether a plug-in is deployed globally or within the context of an adapter. VSI does this by always calling into Oracle Virtual Directory by starting with the next plug-in in the chain after the current plug-in. For example, if there is a mapper before the plug-in, and a cache after the plug-in, then the call to VSI only goes through the cache.

Because the plug-in is now logically in charge of maintaining the bind failure count, the plug-in modify method must be implemented so that any attempt by an LDAP client to modify the count is blocked. The plug-in modify method in Example 18-4 is implemented to throw an exception if the count attribute is included in the modify change list.

Example 18-4 Example modify Method

/**
 * Moves through the "modify" operation's chain
 * 
 * @param chain
 *            The current chain
 * @param creds
 *            The currnet user's credentials
 * @param name
 *            The name of the object being modified
 * @param changeEntries
 *            The group of EntryChange Objects
 */
public void modify(Chain chain, Credentials creds, DirectoryString name,
            Vector changeEntries) throws DirectoryException, ChainException {
 
       Iterator it = changeEntries.iterator();
       while (it.hasNext()) {
             EntryChange ec = (EntryChange) it.next();
             if (ec.getAttr().equals(this.countAttribute)) {
                throw new
                  DirectoryException(LDAPResult.CONSTRAINT_VIOLATION
                  .intValue(), "Can not modify password count attribute");
             }
       }

       chain.nextModify(creds, name, changeEntries);
 
 
}

A DirectoryException is thrown with both a status code and a message. If this exception is not caught by another plug-in, both the message and the code will make it back to the client. For this example, you do not have to check and see if the ignoreOnModify has been configured because you have delegated that decision to the available method. If it was set, the plug-in modify method in Example 18-4 would not have been called.

18.3.3.3.1 Searches

Searches are different then the other operations because there is not one, but three methods that may be implemented. The first method, get, acts as the other operation methods do and allows for the plug-in developer to pre-process the search request and post process the returns. While you could process each Entry returned, it would be very inefficient in terms of memory utilization to do so. Processing results in the get method means that all results must be obtained in memory before they can be returned to the client. For this reason, there is a postSearchEntry method that is executed for every Entry that is returned to the client where attributes can be changed, added, or modified in an efficient matter. There is also the postSearchComplete method that marks that the search operation is complete.

To effectively use the postSearchEntry processing, Oracle Virtual Directory tends to use a special class known as an EntrySet to handle result set processing. A get method returns results by returning a Vector, which includes one or more EntrySets (refer to "Creating EntrySets" for more information).

18.3.4 Creating EntrySets

Each object in a directory is represented in Oracle Virtual Directory by a com.octetstring.vde.Entry object. Each entry contains the name of the object and attributes with attribute values. All entry objects are processed in Oracle Virtual Directory using an implementation of the com.octetstring.vde.EntrySet interface. Entry sets store or handle all entries returned by a particular data source. During normal Oracle Virtual Directory processing, each adapter called during a search request adds its own EntrySet implementation to the list of results to be returned by Oracle Virtual Directory. Additionally, it is also possible that a plug-in could insert additional EntrySet objects into the results vector array. After all adapters have been queried to fulfill the search request, each EntrySet is traversed with its entries sent to the client.

While all adapters produce EntrySet implementations, a plug-in may also create an instance of the EntrySet interface and use it to return entries to the client during a search request.

The following are the two means a plug-in can use to create an EntrySet:

18.3.4.1 ExtensibleEntrySet

The simplest means a plug-in can use to create an EntrySet is by using the com.octetstring.vde.backend.extensible.ExtensibleEntrySet class to create an EntrySet based on a java.util.Vector of Entry objects. The following is the procedure to do so:

  1. Create a new java.util.Vector array.

  2. Add all of the Entry objects to the vector.

  3. Create a new instance of ExtensibleEntrySet passing the above Vector in the constructor.

Example 18-5 shows an example of plug-in using a Web Service to retrieve a stock price based on a stock symbol. The plug-in is designed to implement the concept of a Custom Adapter, which is an adapter that has no functionality itself and is a place holder where adapter level plug-ins can be configured to implement its functions instead. In this stock service example, the plug-in would be configured against a custom adapter. The plug-in is then responsible for handling all events, which means that you would expect that the stock service plug-in would not call the chain.getNext() method.

The get method of the plug-in adds a list of Entry objects (that is, stock prices entries) to a Vector and creates an ExtensibleEntrySet based on that Vector:

Example 18-5 Example EntrySet Creation Using ExtensibleEntrySet

public void get(Chain chain, Credentials creds, DirectoryString base,
            Int8 scope, Filter filter, Bool typesonly, Vector attributes,
            Vector result) throws DirectoryException, ChainException {

     // Since this method is a handler, chain.getNext is not called.

     if (scope.intValue() == SearchScope.BASEOBJECT && base.equals(this.suffix)) {
           // handle the logical root of the adapter
           Entry root = this.getSimpleEntry(this.suffix);
           Vector entries = new Vector();
           entries.add(root);
           result.add(new ExtensibleEntrySet(entries));
           return;
     }

     //This adapter only supports searches based on an equality match 
     //or an or'ing of equality matches
     if (filter.getSelector() != Filter.EQUALITYMATCH_SELECTED &&
           filter.getSelector() != Filter.OR_SELECTED) {
           throw new DirectoryException("Only equality match or an or'ing "+
     "of equality matches are allowed");
     }

     Vector entries = new Vector();

     //If the filter is an OR filter, we can iterate over every quote
     if (filter.getSelector() == Filter.OR_SELECTED) {
           Iterator it = filter.getOr().iterator();
           while (it.hasNext()) {
                 Entry entry = getStockEntry((Filter) it.next());
                 if (entry != null) {
                    entries.add(entry);
                 }
           }
     } else {
            //single quote            
            Entry entry = getStockEntry(filter);
            if (entry != null) {
                  entries.add(entry);
            }
    }
    //We use the ExtensibleEntrySet as a simple holder for entry sets.
    result.add( new ExtensibleEntrySet(entries));             
}

18.3.4.2 Custom EntrySet

While the use of ExtensibleEntrySet is the simplest means to create an EntrySet, it is not the most efficient because it requires that all results be compiled before processing is returned to the client. In this case, a call is made to the service for each term in the filter. A better way to process this request would be to create an EntrySet in such a way as to retrieve new entries as they are requested from the stock service, as in the LDAP Adapter operation.

When the LDAP Adapter EntrySet is asked for the next Entry, the system retrieves the next entry from the remote server one at a time—the way LDAP protocol is intended to work. This approach is more efficient as it allows the client to begin retrieving entries before all entries have been processed. This approach also allows the client to stop retrieval of entries and abort the query.

An implementation of com.octetstring.vde.EntrySet must created for a plug-in to create an EntrySet that returns entries as requested. Each EntrySet must implement the following methods:

  • boolean hasMore()

    Returns true if there are more entries in this EntrySet. This method must be non-destructive.

  • Entry getNext()

    Returns the next entry in the EntrySet or null if there are no more entries.

  • void cancelEntrySet()

    This method is called when an EntrySet cannot be run to completion, allowing a custom EntrySet implementation to release any system resources it was holding.

Example 18-6 is the same plug-in implementation as Example 18-5 that creates an adapter out of a stock ticker Web Service, however, in Example 18-6, the get method only creates a list of symbols which gets passed off to the custom EntrySet. The get method in Example 18-6 adds a list of Entry objects (that is, stock prices entries) to a Vector and creates an ExtensibleEntrySet based on that Vector:

Example 18-6 Example get Method That Passes to a Custom EntrySet

public void get(Chain chain, Credentials creds, DirectoryString base,
            Int8 scope, Filter filter, Bool typesonly, Vector attributes,
            Vector result) throws DirectoryException, ChainException {
     if (scope.intValue() == SearchScope.BASEOBJECT && base.equals(this.suffix)) {
            Entry root = this.getSimpleEntry(this.suffix);
            Vector entries = new Vector();
            entries.add(root);
            result.add(new ExtensibleEntrySet(entries));
            return;
     }

     //This adapter only supports searches based on an equality match 
//or an or'ing of equality matches
     if (filter.getSelector() != Filter.EQUALITYMATCH_SELECTED &&
 filter.getSelector() != Filter.OR_SELECTED) {
     throw new DirectoryException("Only equality match or an or'ing"+
                                  " of equality matches are allowed");
     }

     String rdn="uid";
     ArrayList symbols = new ArrayList();
     //If the filter is an OR filter, we can iterate over every quote
     if (filter.getSelector() == Filter.OR_SELECTED) {
           Iterator it = filter.getOr().iterator();
           while (it.hasNext()) {
                     //Extract the symbol from the filter
                  String symbol = new String(filter.getEqualityMatch().
                  getAssertionValue().toByteArray());

                  //The attribute being checked in the equality search
                  //doesn't really matter, but we need an RDN for each entry
                  rdn = new String(filter.getEqualityMatch().
                  getAttributeDesc().toByteArray());
                  symbols.add(symbol);
           }
     } else {
            //single quote
            //Extract the symbol from the filter
            String symbol = new String(filter.getEqualityMatch().
            getAssertionValue().toByteArray());

           //The attribute being checked in the equality search doesn't
           //really matter, but we need an RDN for each entry
           rdn = new String(filter.getEqualityMatch().
                 getAttributeDesc().toByteArray());
           symbols.add(symbol);
       }

       //We use the ExtensibleEntrySet as a simple holder for entry sets.
       result.add( new StockEntrySet(symbols.iterator(),rdn,this.base));

}

In Example 18-6, a list of stock symbols is created by iterating over the or in the search filter. The compiled list is passed to the custom EntrySet implementation as shown in Example 18-7:

Example 18-7 Example of Data Passed to Custom EntrySet

public class StockEntrySet implements EntrySet {

       Iterator quotes;
       String rdn;
       String base;

       public StockEntrySet(Iterator quotes, String rdn,String base) {
              this.rdn = rdn;
              this.quotes = quotes;
              this.base = base;
       }

       public Entry getNext() throws DirectoryException {
              Entry entry = this.getStockEntry((String) quotes.next());
              if (entry == null) {
                    if (this.hasMore()) {
                          return this.getNext();
                    } else {
                          return null;
                    }
              } else {
                     return entry;
              }
       }

       public boolean hasMore() {
              return quotes.hasNext();
       }

       /**
       * Returns an entry for a stock quote
       * @param filter
       * @return An entry for the stock quote, or null for none.
       * @throws DirectoryException
       */
       public Entry getStockEntry(String symbol) throws DirectoryException {
       //Create a new entry with the symbol as the RDN
            Entry entry = new Entry(new DirectoryString(rdn + "=" + symbol + "," +
 this.base));

       //This uses an Apache Axis generated client stub
       NetXmethodsServicesStockquoteStockQuoteService service = new
       NetXmethodsServicesStockquoteStockQuoteServiceLocator();
       try {
            NetXmethodsServicesStockquoteStockQuotePortType 
            quoteService = service.
                 getNetXmethodsServicesStockquoteStockQuotePort();
            double value = quoteService.getQuote(symbol);
            if (value == -1) {
                  return null;
            }

            //Create the attribute for the entry
            Vector vals = new Vector();
            vals.add(new DirectoryString(symbol));
            entry.put(new DirectoryString(rdn),vals);

            vals = new Vector();
            vals.add(new DirectoryString("top"));
            vals.add(new DirectoryString("stockForOrganization"));
            entry.put(new DirectoryString("objectClass"),vals);

            vals = new Vector();
            vals.add(new DirectoryString(Double.toString(value)));
            entry.put(new DirectoryString("quote"),vals);

            return entry;

       } catch (ServiceException e) {
             throw new DirectoryException("Could not load web service : " +
 e.getMessage());
       } catch (RemoteException e) {
            throw new DirectoryException("Could not load web service : " +
 e.getMessage());
       }
}
    public void cancelEntrySet() {
    // nothing to do
    }
}

The EntrySet implementation in Example 18-7 uses a java.util.Iterator to track which symbol is currently being processed. The StockEntrySet class does not call out to the Web Service to create entry results until an Entry is requested by Oracle Virtual Directory on behalf of the client.

Because the plug-in supports searching one or more stocks, it is possible that not all searches return valid results. Consider that if getNext returns a NULL result to Oracle Virtual Directory before the list of stocks is exhausted, Oracle Virtual Directory prematurely assumes the results are exhausted. To handle this situation, an extra block of code is added to getNext after the call to getStockEntry. If getStockEntry returns a NULL and the iteration through the requested stocks has not finished, getNext calls itself to process the next candidate. This recursion continues until at least one valid result is returned or all queries are exhausted.

18.3.5 Understanding Filter Processing

LDAP filter possessing can be complicated. In the context of an Oracle Virtual Directory plug-in, there are two instances when it may be useful to parse a filter: pre-process or post-process. Each method offers its own advantages and disadvantages and is not always mutually exclusive.

Post-Process Filtering

In post-process filtering, the com.octetstring.vde.util.FilterUtils.evalFilter(Entry e, Filter f) method is used to see if an entry being returned as a result matches a required filter. This is the simplest way to handle filters and is useful when you are dealing with a small predefined data set that can remain in memory as a collection of Entry objects. This method is not generally the best solution when a filter must be translated into another format, for example, into a SQL WHERE clause or a special object model for an external API.

Pre-Process Filtering

Pre-processing filters are used to parse a filter and to apply it to a modified search or transform it to another format that the target of the search can understand. Think of pre-processing filters as converting an LDAP filter to an SQL WHERE clause, which to do so, you must traverse the filter object. For example, consider the conversion of the following LDAP filter to an SQL WHERE clause:

(&(|(user=jsmith)(user=lswanson)(user=ccarson))(dept=payroll)) 

The preceding LDAP filter states All records where the user is jsmith, lswanson or ccarson and whose department is payroll. Figure 18-1 shows a visual representation of this LDAP filter:

Figure 18-1 Visual Representation of an Example LDAP Filter

Visual representation of an LDAP filter

To translate the filter shown in Figure 18-1 into an SQL WHERE clause, you use a recursive function that traverses the tree. The example filter is represented in Oracle Virtual Directory as a hierarchy of Filter objects, which contain collections of other Filter objects to create a traversable tree. The filter shown in Figure 18-1 will have the object model shown in Figure 18-2, where the name of the class used to represent the filter element is below the operation or operand:

Figure 18-2 Example Object Model of a Filter

Example Object Model

To traverse the tree shown in Figure 18-2, a recursive method is used that queries the getSelector() method of the filter to determine what type of filter it is. After the type for the filter is determined, its value must be extracted by using a getFilterType method. For example, if the filter is an equality filter, such as user=jsmith, the value of the filter object would come from currentFilter.getEqualityMatch(). In this case the return value is an AttributeValueAssertion, which stores the attribute name and value as an Oracle. Once retrieved, the values can be converted into String objects. Filter_and and Filter_or objects return java.util.Iterator classes for iterating through the child filters that are being operated on.

LDAP filters do not limit you to two terms per relation. The OR portion has three operands. Since SQL only allows two operands per operation, the tree in Figure 18-2 must be converted to a binary tree.

Figure 18-3 Breaking an OR Function

Breaking an OR function.

In Figure 18-3, the OR operation is broken up into two separate OR operations. The final WHERE clause from the filter is ((user=jsmith) OR ((user=lswanson) OR (user=ccarson))) AND (dept=payroll). The LDAP prefix notation has been transformed into a SQL like infix notation with only two operands per operation. Example 18-8 shows the source code for the transformation:

Example 18-8 Example Source Code for Transforming an LDAP Prefix Notation to SQL Notation

import com.octetstring.vde.util.*;
import com.octetstring.ldapv3.*;
import java.util.*;

public class ConvertFilter {
    public static void main(String[] args) throws Exception {
        String ldapFilter = "(&(|(user=jsmith)(user=lswanson)" +
                                 (user=ccarson))(dept=payroll))";

        System.out.println("Ldap Filter : " + ldapFilter);
        System.out.println("SQL WHERE : " +
            filterToSQL(ParseFilter.parse(ldapFilter)));
    }
    /**
     *Converts an ldap filter to an SQL WERE clause
     *@param currentFilter The filter being converted
     */
public static String filterToSQL(Filter currentFilter) {
        String[] filterVal;
        String infix="";
        switch (currentFilter.getSelector()) {
            case Filter.EQUALITYMATCH_SELECTED :  // (attrib=val)
                 filterVal = getString(currentFilter.getEqualityMatch());
                 return filterVal[0] + "=" + filterVal[1]; 
                 
             case Filter.PRESENT_SELECTED : // (attrib=*)
                 return new String(currentFilter.getPresent().toByteArray()) +
                        "=*"; 
                  
             case Filter.GREATEROREQUAL_SELECTED : // (attrib>=val)
                 filterVal = getString(currentFilter.getGreaterOrEqual());
                 return filterVal[0] + ">=" + filterVal[1];

             case Filter.LESSOREQUAL_SELECTED :  // (attrib<=val)
                 filterVal = getString(currentFilter.getLessOrEqual());
                 return filterVal[0] + "<=" + filterVal[1]; 

             case Filter.SUBSTRINGS_SELECTED : // (attrib=val*ue)
                 filterVal = getString(currentFilter.getLessOrEqual());
                 return filterVal[0] + " LIKE " + filterVal[1];

             case Filter.AND_SELECTED : // &((attrib=val)(attrib2=val2))
                 Filter_and andFilter = currentFilter.getAnd();
                 
                 infix = "";
                 for (Iterator andEnum = andFilter.iterator();
                        andEnum.hasNext();) {
                    Filter aFilter = (Filter) andEnum.next();
                    infix += "(" + filterToSQL(aFilter) + ") AND ";  
                 }
                  
                 infix = infix.substring(0,infix.lastIndexOf("AND")) + " ";
                 return infix;

             case Filter.OR_SELECTED : // &((attrib=val)(attrib2=val2))
                 Filter_or orFilter = currentFilter.getOr();
                 infix = "";
                 for (Iterator orEnum = orFilter.iterator();orEnum.hasNext();)
                    {
                        Filter aFilter = (Filter) orEnum.next();
                    infix += " ( " + filterToSQL(aFilter) + " ) OR ";
                 }
                 infix = infix.substring(0,infix.lastIndexOf("OR")) + " ";
                 return infix;
                case Filter.NOT_SELECTED : // !(&((attrib=val)(attrib2=val2)))
                    return " NOT (" + filterToSQL(currentFilter.getNot()) + 
                            ") ";

                case Filter.APPROXMATCH_SELECTED : // (attrib~=val)
                    filterVal = getString(currentFilter.getApproxMatch());
                    return filterVal[0] + " LIKE " + filterVal[1];

                case Filter.EXTENSIBLEMATCH_SELECTED : //not standard
                    return ""; //not supported
        }
        
        //will never reach
        return "";
    }
    
    /**
      *Converts an AttributeValueAssertion to a two element array with the 
      *first being the attribute name and the second being the value
      */
    public static String[] getString(AttributeValueAssertion ava) {
        String matchAttr = new String(ava.getAttributeDesc().toByteArray());
        String matchVal = new  
                String(ava.getAssertionValue().toByteArray(),"UTF8");
        
        return new String[] {matchAttr,matchVal};
    }
}

18.3.6 Understanding Classes

The topics in this section provide a high-level introduction to the Oracle Virtual Directory classes that are available. Refer to the Oracle Virtual Directory Javadoc for complete information about Oracle Virtual Directory classes. The topics in this section include

18.3.6.1 Virtual Service Interface

The Virtual Service Interface (VSI) provides methods to make LDAP-like calls into the Oracle Virtual Directory. VSI works the same way regardless of whether the context is a global plug-in or an adapter level plug-in.

If there are three plug-ins in a chain and the first plug-in in the chain calls into VSI, then the context of the call is Oracle Virtual Directory as it appears through the second and third plug-in. If the call comes from the second plug-in, then it would only go through the third plug-in. If the call originates from the third plug-in, the call does not go through any plug-ins, regardless if the plug-in is global or local. If the plug-in is global, then the call continues out of the global chain, through the Oracle Virtual Directory routing system into adapter level chains (depending on whether one or more adapters are selected by the router), which not only guarantees that calls into Oracle Virtual Directory are consistent, but also protects against infinite loops of a plug-in calling itself. The VSI is retrieved by using the chain object which is passed into every plug-in method.

18.3.6.2 Global Service Interface

The Global Service Interface (GSI) provides methods to make LDAP-like calls into the Oracle Virtual Directory as though they were coming from a end client. Each call is processed through the access control system (if enabled) and offers the ability to let the router select appropriate adapters for an operation. The GSI is the same interface that the LDAP Listener and Web Gateway use to communicate.

GSI can be retrieved by using the VSI by calling chain.getVSI().getGSI(). With this handle, the add, bind, delete, get, getByDN, modify, and rename methods can be called.

Warning:

With the GSI, it is possible for a plug-in to be caught in an infinite loop if it calls to a context above the current plug-in. Doing this can cause a scenario where the plug-in code is called repeatedly causing unanticipated results. Unless you intend for this to happen, be careful of scenarios where plug-ins call up the stack where looping might occur. In general, unless you must call a specific adapter, it is always safest to use VSI.

Oracle Virtual Directory provides no loop detection mechanisms. If you find that Oracle Virtual Directory has crashed with a custom plug-in due to a stack overflow or memory exhaustion, this is the most likely cause.

18.3.6.3 Adapter Service Interface

The Adapter Service Interface (ASI) provides methods to make LDAP-like calls into the Oracle Virtual Directory at the router level or directly to a specific adapter. The Oracle Virtual Directory Join View Adapter and its Joiners use ASI to communicate with adapters that are being searched and joined. The ASI interface is useful when you want to obtain information from an internal adapter, such as when configured to provide look-up information for a plug-in class.

ASI is retrieved through the VSI by calling chain.getVSI().getASI(). With this handle, the add, bind, delete, get, getByDN, modify, and rename methods can be called. Each method has two variations: one that provides a parameter for an adapter name, and another without. Use the method with adapter names to select specific adapters or use the other, nameless method to let the router select the appropriate adapters for you based on routing logic and routing configuration.

Warning:

With the ASI, it is possible for a plug-in to be caught in an infinite loop if it calls to a context above the current plug-in. Doing this can cause a scenario where the plug-in code is called repeatedly causing unanticipated results. Unless you intend for this to happen, be careful of scenarios where plug-ins call up the stack where looping might occur. In general, unless you must call a specific adapter, it is always safest to use VSI.

Oracle Virtual Directory provides no loop detection mechanisms. If you find that Oracle Virtual Directory has crashed with a custom plug-in due to a stack overflow or memory exhaustion, this is the most likely cause.

VSI, GSI and ASI all share a common interface, with certain interfaces providing extra functionality.

The following list describes the supported ASI methods:

  • add()

    Performs an LDAP add operation. Two versions of this method allow either the Oracle Virtual Directory Router to select the target adapter, or a specific adapter can be selected.

  • bind()

    Performs an LDAP bind operation, either letting the Oracle Virtual Directory Router choose the adapter or applying to a specific adapter.

  • delete()

    Performs an LDAP delete operation either letting the Oracle Virtual Directory Router choose the adapter or applying to a specific adapter.

  • get()

    Performs an LDAP get operation letting the Oracle Virtual Directory Router choose eligible adapters. The get method returns a java.util.Vector of EntrySet values. An EntrySet is included for each adapter that was queried.

  • getbyDN()

    A convenience method that performs an LDAP base search using a specific DN. The caller may choose to specify a specific adapter or may let the Oracle Virtual Directory Router choose.

  • modify()

    Performs an LDAP modify operation. The caller may specify a specific adapter or may elect to have the Router choose automatically.

  • rename()

    Performs an LDAP rename operation. The caller may specify specific from and to adapters or may elect to have the Router choose automatically.

18.3.6.4 Joiner

The Oracle Virtual Directory Join View Adapter uses Joiners to join entries from a specific adapter and to merge them with entries from a primary adapter. A Joiner is an abstract class that defines the basic operations and methods required to implement a new Joiner. Joiners are called by the Join View Adapter whenever operations must be performed against a joined entry. Joiners define pre-action operations to allow manipulation of data before any LDAP operation. Joiners also define mapOperationTargetByEntry methods that allow it to select a target entry in the target joined adapter depending on the operation being called.

A Joiner is instantiated with a primary adapter and a target adapter. The Join View Adapter always works in the context of the primary adapter and calls Joiner methods when mapping and when manipulations must be performed on a target joined adapter.

The get operation of the Join View Adapter builds a JoinEntrySet based solely on results from the primary adapter. As the Oracle Virtual Directory client subsequently polls for results from the Oracle Virtual Directory, the JoinEntrySet class calls the joiner JoinByEntry method to make a call to the joined adapter and merge the entry results. If you configure multiple join relationships, the entry set processing loops through all of the joins until the entry is fully joined based on all defined relationships.

The Joiner constructor method is called when the Joiner is instantiated by the Join View Adapter. This does not happen until the first LDAP operation is processed by the Join View Adapter (a form of lazy construction). The constructor is passed the configuration parameters for the joiner from the configuration file along with the associated target adapter name.

The createJoinFilter method is usually a local method called by the JoinByEntry method to create a search filter for a subsequent call to the AdapterServiceInterface.

18.3.6.5 Utility Classes

Oracle Virtual Directory supports the following utility classes:

  • PluginUtils

    This class is a basic toolbox of mapping functions including renameAttribute, copyAttribute, and others. These classes are normally used in mapping scripts but are available for use in Java Plug-ins.

  • FilterTools

    This class provides methods for creating and manipulating LDAP search filters.

  • ParseFilter

    The ParseFilter class provides ability to convert a String to a Filter and back again.

  • DNUtility

    This class provides DN manipulation routines such as explode and create dn allowing manipulation of individual DN name components.

  • LDAPResult

    The LDAPResult is a utility class that you can use to compare the results returned from any method that returns an Int8 value. These constants help you translate result codes into LDAP error codes.

  • VDELogger

    The Logger class provides an interface into the Oracle Virtual Directory logging facility (Log4J). Use this class to integrate your console or audit messages with those of Oracle Virtual Directory.

  • PasswordEncryptor

    This class provides various methods to encrypt string values into various hashed formats including Crypt, SHA, SSHA.

18.3.6.6 Data Classes

Oracle Virtual Directory supports the following utility classes:

  • Attribute

    Attribute is a basic object used with the Entry class. An attribute defines a type (as in the attribute name) and contains its values. Methods are also provided for cloning and equivalence testing.

  • Credentials

    A basic object holding the credentials of a session. The IP address, binddn, and password if needed, are in this object. Normally, for most operations relating to the AdapterServiceInterface, only binddn is relevant.

  • Entry

    This object is used to hold an LDAP entry and it is used to contain partial entries such as with an LDAP modify request. The FilterTool utilities often work with these objects to test filters.

  • EntryChange

    This object contains an LDAP modify item. When handling modification requests, usually a Vector of EntryChange objects are passed to the AdapterServiceInterface. Each EntryChange contains a single modification to a single entry.

  • EntrySet

    An EntrySet contains a set of query results from an adapter. When a method first receives an EntrySet, the entire result set may not be in memory. Unique Entry objects are returned from an EntrySet by calling its getNext method. Each time getNext is called, the relevant adapter or plug-in class code is called to retrieve the next Entry if there is one. To test the availability of another entry, use the hasMore() method.

    Tip:

    Unless you intend to process an entire result set, you should avoid calling getNext() directly. It is always better to let the LDAP client do this. For an Oracle Virtual Directory plug-in class, a special method, postSearchEntry(), is provided giving the ability to modify each entry as it is returned to the client. Needlessly calling getNext()can cause excessive memory use and performance loss because Oracle Virtual Directory is required to load an entire result set at once, rather than process entries as they arrive from the adapters.

  • Filter

    The Filter object is a representation of a standard LDAP filter. This object provides useful methods for setting, testing, and comparing LDAP filters. The Filter object may contain a hierarchy of other filter objects (for example, Filter_and, Filter_or).

  • LDAPURL

    This class provides methods to parse a standard LDAPURL or to create one.

18.3.6.7 Data Types

Oracle Virtual Directory supports the following data types:

  • BinarySyntax

    Any binary value such as a password or user certificate.

  • DirectoryString

    A case-insensitive string.

  • IASString

    A case-sensitive string.

  • IntegerSyntax

    An integer value

  • DistinguishedName

    A distinguished name value (comparisons follow DN equality rules).

18.3.6.8 Exceptions

Oracle Virtual Directory supports the following exceptions:

  • DirectoryBindException

    Exception thrown when a bind is unsuccessful.

  • DirectoryException

    A general exception thrown during any directory error. Check getMessage() for more information, or getLDAPErrorCode() to determine the LDAP error code. This exception may be generated by an adapter or by another plug-in.

  • DirectorySchemaViolation

    A schema violation occurs when an attempted add or modify of an entry that violates either remote schema or local schema.

  • InvalidDNException

    An InvalidDNException is thrown by objects and utilities whenever an invalid DN is passed as a parameter.

18.4 Connecting Web Service Clients to Oracle Virtual Directory

When Oracle Virtual Directory was first released, LDAP was the dominant protocol for accessing identity profile information. While LDAP is still the dominant protocol for authentication and authorization, new applications are often built by using Web services based on SOAP or REST standards. Oracle Virtual Directory provides mechanisms to meet these requirements by default.

REST-based clients (in simple terms, client applications that send an HTTP POST and get back data) are handled by the Oracle Virtual Directory Web Gateway (see Appendix C, "HTTP Listener's Web Gateway Service").

As an alternative, you can use Oracle Virtual Directory's DSMLv2 service, which is Oracle's implementation of the DSMLv2 standard. DSML was initially created to provide an XML representation of LDAP data (basically an alternative to ASCII-based LDIF). DSMLv2 added a SOAP-based Web service.

This SOAP-based Web service is available in Oracle Virtual Directory if you enable the DSMLv2 service in a HTTP listener. The URL is

http://ovdserver:httpport/services/dsmlv2/service

Unfortunately, while DSML is a standard defined by the OASIS standard body and it is a SOAP-based service, an official WSDL file was never produced. A WSDL file is a document used in SOAP-based Web services to describe those services to client applications so that the applications know what methods to call.

However, a third party has developed a WSDL file, which is available at

http://www.users.globalnet.co.uk/~jonbek/EASBlogLinks/dsmlQuery_v3.wsdl

About Authentication

Oracle Virtual Directory's DSMLv2 service honors all of the Oracle Virtual Directory security semantics such as ACL, routing rules, etc.

Applications authenticate to the DSMLv2 service using HTTP Basic authentication. HTTP Basic authentication does not use the .htaccess files used by the Oracle Virtual Directory Web Gateway.

To send credentials to the DSMLv2 service, the SOAP client should send an HTTP Authorization header containing the following values:

base64-encoded-dn:base64-encoded-password

For example, assuming the user is cn=orcladmin and password is welcome1, the credentials would look like this:

Y249b3JjbGFkbWlu:d2VsY29tZTE=

This string also must be base64-encoded, so the complete header would look like this:

Authorization: Basic WTI0OWIzSmpiR0ZrYldsdTpkMlZzWTI5dFpURT0=

As long as you provide a valid DN or credentials, Oracle Virtual Directory security is used as if you were accessing Oracle Virtual Directory through an LDAP client.