LDAP Connect User's Guide

CHAPTER 5

Working with LDAP and DSML

This chapter is designed to familiarize you with LDAP programming idioms as they apply to the LDAP Connect for Composer, and show how various advanced LDAP and Composer features can be used together, with emphasis on testing and debugging.

 
Top of page

DSE Query Example

A useful discovery mechanism (and audit capability) afforded by LDAP is the ability to query the DSE root, or DSA-specific entry, of a directory server. (DSA means directory system agent, which is X.500 jargon for a directory server.)

If a server supports LDAP version 3, the DSE root can be queried for "meta" information about the server. The DSE root entry isn't really an addressable object, per se, but it can be queried through a special syntax (which we'll see in a minute).

By querying the DSE entry, you can find out (among other things):

The following example shows the steps involved in building a component that queries the DSE entry of Novell's public server at www.nldap.com. But instead of recapitulating the detailed step-by-step procedure for building each action and each resource (already covered in previous sections), we'll concentrate on the larger workflow issues related to building an LDAP Component, such as how to test and debug the component as part of the design session.

 
Top of section

Connection Resource for Anonymous Bind

First, we need to have a connection resource. The LDAP Connection Resource for this example will use the following settings. Notice that no password is given. The bind is thus (by definition) anonymous.

5AnonConnectRes

 
Top of section

Component and Action Model

Next, we'll create a new LDAP Component called DSETest, containing two actions: a Create DSML Action (of the Search request type) and an Execute DSML Action.

The search parameters for a DSE root dump are straightforward: All that's needed is a base-level search on an empty Base DN, with a filter set to:

(objectClass=*)

We will set the Native Environment Pane's Search tab something like this:

5BuggyQuery

NOTE:   This example purposely contains a bug. The settings shown above will cause an error. (Can you spot the problem?)

The Search tab will look like:

5Searchtab

We want to receive information on all attributes, hence we will enter nothing in the Attributes tab. (Just accept the defaults.) Notice that the Selected pane (below, right) is empty. When this list is empty, the server, by default, returns all attributes that apply to the "hits" turned up by the query.

5attribs

For test purposes, we will simply map DSML (from the Create action) into Input and map our response straight to Output. Therefore, the action model will initially look like this:

5ActionModel1

To test the actions, we can simply click the Execute All tool icon in the main toolbar.

4ExecuteAll

When the component executes, the Output DOM populates with data. But on close inspection, we see that there has been a problem. The Output document looks rather short and the resultCode is not zero:

5DOMsAfterError

The error description (at the bottom of the Output DOM) is "Invalid DN Syntax." And this is reflected in the Input DOM tree, next to dn, under searchRequest, where the data value is org.mozilla.javascript.Undefined@3cd5b5.

The problem: The Base DN value in the Search tab has to be valid ECMAScript. We left the field blank. Instead, we should have specified an empty string: two quotation marks with nothing in between them.

The fix is easy: Go back and put two quotation marks in the Base DN field. But before pressing the Execute All button again, we need to clean out the existing DOMs. To do this, go to the main menubar and choose Component > Reload XML Documents:

4reloadXMLDocs

The DOM windows will reset to their original (empty) state.

NOTE:   If you fail to reset the DOMs, the next round of execution will cause the Create DSML action to simply append more data (another request) onto the existing Input document. After the component executes, your Output document will contain the responses for two requests: the original (unsuccessful) request and the one that was appended to Input.

When we rerun the component, this time it works without error. The Output DOM is quite long and contains many attr elements corresponding to DSE root entries:

5LongOutputDOM

 
Top of section

Dealing with Errors

As the above example shows, unsuccessful LDAP or DSML operations don't result in errors, per se, at the action-model level. If an LDAP exception occurs in the context of a query, it's merely reported back to the client in the DSML document under the resultCode element. Your application may want to know about the exceptional condition and take special action; or it may simply need to pass the information on through. If it needs to know about the unsuccessful nature of the query, you will need to add logic to deal with this.

As an example, suppose you want your application to log unsuccessful DSML requests. One way to implement this would be to include logic in the action model to the effect: "If the result was anything other than success, log the reason to System output [or a logfile]."

Here is how you could accomplish this:

Procedure To log query failure notices\

  1. Position the cursor inside the action pane (action model) at the point where you want a new action to appear.

  2. Use the Action menu in the main menu bar to select New Action > Function. The Function Action dialog appears.

  3. Click the small `E' icon to open the Expression Builder dialog.

    5ExprBuilder

  4. In the Expression Builder, click into the Output node tree until you have exposed the resultCode node and its children. Doubleclick the code attribute node. An ECMAScript expression appears in the text-edit pane.

    5ExprBuilder2

    NOTE:   This example assumes that you have a DSML document already loaded in your Output DOM. Construct and run a Create DSML and Execute DSML action, if need be, so that you will have a fully browseable Output DOM (similar to that shown) in the Expression Builder picktree.

    The expression gives the XPath syntax for the resultCode, wrapped in a call to the Composer extension method XPath( ). The Expression Builder gives you a visual, point-and-click way of building XPath-related ECMAScript expressions so that you don't have to type them in (and debug them) manually.

  5. Type "theResultCode = " (minus quotes) in front of the ECMAScript expression. (This is just a shorthand way of storing the result code value in a script variable.)

  6. Click OK to go back to the Function Action dialog. The edit window of the dialog should contain the statement:

      theResultCode = 
      Output.XPath("batchResponse/searchResponse/searchResultDone/resultCode/@code");
    

    (It should be all one line. Ignore the linewrap.)

  7. Click OK or type Enter to dismiss the Function Action dialog. A new action appears in the action model.

    Use the Action menu in the main menu bar to select New Action > Decision. The Decision Action dialog appears.

    5Decision

  8. Type "theResultCode != 0" (as shown above) into the box. This is the truth condition for the Condition. If theResultCode is zero (success), the condition is false. Any value other than zero will make the condition true.

  9. Click OK or type Enter to dismiss the dialog. A new Decision action appears in the action model.

  10. TRUE and FALSE statements will appear on their own lines after the Decision action. Single-click on the TRUE line.

  11. Use the Action menu in the main menu bar to select New Action > Log. The Log Action dialog appears.

    5Log

  12. Choose one of the radio buttons under Log to, to specify a destination for any messages logged by this action. We've chosen System Output for test purposes, since (in Composer) this means the log message will show up in the Log tab at the bottom of the main Composer window, where it can be seen easily during design-time testing. For runtime purposes, you would probably want to (re)set this to a User or System Log.

  13. Enter whatever descriptive text you would like to appear in the log message for this action. (Remember, this is in the TRUE branch, which means this message will be logged whenever the query result was non-zero—i.e., non-successful.) In this example, we used the Expression Builder to help construct an ECMAScript statement that finds the plaintext error message in the Output DSML document.

  14. Click OK to dismiss the dialog.

  15. Single-click on the FALSE line in the action model under the Decision action, and repeat the foregoing steps to create a Log Action for the FALSE branch, if desired. (This is the branch corresponding to success, not failure, of a query.)

    The action model, at this point, should look something like this:

    5DSETestActionModel

  16. In the main menu bar, choose Component > Reload XML Documents (to purge your DOM windows), then Execute the component, or its individual actions, to test it.

  17. Save your work.

 
Top of page

ECMAScript and the LDAP Connect

Composer's ECMAScript binding provides a powerful and flexible tool for performing programmatic operations of various sorts, including straight-through calls to Java objects. Since the LDAP Connect for Composer uses Novell's JLDAP library (already included in the CLASSPATH for your projects), you can easily call straight into the Java-level LDAP API. An example of this is shown further below.

 
Top of section

LDAP Extension Methods

When you are using the LDAP Component editor to create or edit an action model, several LDAP-related Composer extension methods are available for use in Function actions and other places where script expressions are permitted. The method you will use most often is getLDAPAttr( ), described below.

getLDAPAttr(String connResource, String dn, String attr)—Looks up a value stored in a particular attribute of a named object in an LDAP directory, using the connection resource whose name is supplied in the first argument. The second argument is the object's LDAP distinguished name. The third arg is the attribute of interest. The value returned may be numeric or String data. Use ECMAScript's typeof operator to determine if the value is of type "number" versus type "string."

 
Top of section

Access Control List (ACL) Methods

Access Control Lists are used in Novell eDirectory as a way of managing access to directory information based on identities and privileges. Access control is implemented by an optional, multivalued attribute called ACL, which is defined on the top-level directory object called "top." Since all directory objects inherit from the top object, all objects can use the ACL attribute.

Each value stored in a tree entry's ACL attribute represents information about a trustee (a different object) whose access to the entry is to be controlled. In other words, the ACL stores information about client objects (accessors) rather than about the data-store object itself.

Composer's LDAP Component Editor does not expose UI tools for managing ACLs and privilege sets—for good reason. ACLs are at the heart of directory security. ACL-editing capability is not something you'd want to expose in a web service. Nevertheless, there may be situations in which it is necessary or desirable to set trustee characteristics on an object "under the covers," as part of the normal execution of a service. You can do this in Composer with ECMAScript.

Two ECMAScript extension methods are available for creating ACL (Access Control List) values that can be attached to objects in an eDirectory tree.

NOTE:   These methods are relevant only to Novell eDirectory and NDS-compliant directory servers.

ndsACL.createEntryACL(boolean abBrowse, boolean abAdd, boolean abDelete, boolean abRename, boolean abSupervisor, boolean abInherit, String asTrusteeDN)

Creates a properly formatted ACL value that can be added to the (optional, multivalued) ACL attribute of any object in a Novell eDirectory tree. The return value of this method represents a specific set of Access Control List privileges applicable to a particular "asTrusteeDN" accessor. The first six parameters are true/false flags indicating the rights to be granted. The "abInherit" argument determines whether children of the trustee object should inherit the rights of the parent.

ndsACL.createAttrACL(boolean abCompare, boolean abRead, boolean abWrite, boolean abSelf, boolean abSupervisor, boolean abInherit, String asTrusteeDN, String asProtectedAttrName )

Similar to the previous method, but creates access control policy for a given attribute on an existing object.

The general procedure for using these methods is:

An example will help make this clearer: Suppose you have, in your eDirectory tree, an inetOrgPerson whose common name (CN) is Bob. You want Bob's telephoneNumber attribute to be exposed to another inetOrgPerson, named Jill. To do this, you would create an access control value using ndsACL.createAttrACL(), specifying Jill's DN as the trustee DN and telephoneNumber as the protected attribute name. In a Modify operation, you add the access control value to Bob's ACL attribute. (Remember, ACL is a multivalued attribute. Bob may have scores or even hundreds of values stored under his ACL attribute.) Jill would then have certain specific access rights to Bob's telephone number.

CAUTION:    ACLs are a critical component of eDirectory security. You should have a thorough understanding of ACL concepts before attempting to use the above methods. Misuse of ACL methods can cause security to be compromised at many different levels of a directory tree. Do not attempt ACL modifications unless you are confident that you thoroughly understand NDS security concepts and their implications.

For a technical overview of ACL concepts, consult the NDS Technical Overview document (in particular, the chapter on eDirectory Security) available online at http://developer.novell.com/ndk.

 
Top of section

Access to Novell LDAP Classes

The Novell Java LDAP (JLDAP) library classes, which are publicly available as part of the Novell NDK, are already a part of Composer (design-time as well as runtime) and are already in the classpath. Therefore you can write code of the following sort in Composer's Custom Script Resource editor (or in the Expression Builder dialogs, etc.):

  myConn = new Packages.com.novell.ldap.LDAPConnection();

Notice that in ECMAScript, access to the Java classloader occurs via the Packages keyword. Hence, to access any class that is visible on the classpath, you need only prepend "Packages" to the qualified class name, as shown above. Since ECMAScript variables do not require a data type declaration, it is perfectly legal to execute the above line of code as-is. It both declares and initializes the variable myConn in one operation.

 
Top of page

ECMAScript Example Involving LDIF

This section shows an example of how to use ECMAScript to call Novell LDAP classes directly, so as to accomplish tasks that would otherwise not be possible in the LDAP Component Editor. The example shown below involves LDIF.

Prior to the advent of DSML, directory users relied heavily (and still do) on the text-based LDIF (LDAP Data Interchange Format) file type for persisting LDAP objects and queries. (The LDIF specification is published in RFC 2849.) LDIF is convenient for many of the same reasons XML is: It's human readable, structured according to relatively simple rules, platform-neutral, etc. Many administrative tools are able to handle LDIF natively.

It is useful to compare the DSML and LDIF versions of a query result. The DSML version might look something like this:

<?xml version="1.0" encoding="UTF-8"?>

<batchResponse xmlns="urn:oasis:names:tc:DSML:2:0:core">

<searchResponse requestID="160">

<searchResultEntry dn="cn=admin,ou=lapdog,ou=user,o=NOVELL" requestID="160">

<attr name="cn">

<value>admin</value>

</attr>

<attr name="loginTime">

<value>20030315051622Z</value>

</attr>

<attr name="objectClass">

<value>inetOrgPerson</value>

<value>organizationalPerson</value>

<value>person</value>

<value>top</value>

<value>ndsLoginProperties</value>

</attr>

<attr name="securityEquals">

<value>ou=lapdog,ou=user,o=NOVELL</value>

...

[ etc. ]


The LDIF version of the same thing would look like this:

version: 1


dn: cn=admin,ou=lapdog,ou=user,o=NOVELL

cn: admin

loginTime: 20030315051622Z

objectClass: inetOrgPerson

objectClass: organizationalPerson

objectClass: person

objectClass: top

objectClass: ndsLoginProperties

securityEquals: ou=lapdog,ou=user,o=NOVELL

. . .

[ etc. ]

The structure of an LDIF file is not unlike that of a simple flat file with one record per line and each record representing an attribute-value pair delimited by a colon. (The syntax isn't quite that simple, but close enough for this discussion.)

For audit purposes, it's sometimes convenient to obtain an "LDIF dump" of a particular tree object. It doesn't take much ECMAScript to do this.

To programmatically query a directory and write the results to an LDIF file, start by creating a Custom Script Resource (using Composer's main menu bar, go to File > New > xObject then open the Resource tab and select Custom Script). Inside the custom script editor window, enter a custom function like that shown below.

  function query2Ldif( ldapHost, 
  	       loginDN, 
  	       password,
  	       searchBase,
  	       scope,
  	       searchFilter,
  	       attribs,
  	       fileName )
  {
         var jldap       = Packages.com.novell.ldap;
         var ldapPort    = jldap.LDAPConnection.DEFAULT_PORT;
         var ldapVersion = jldap.LDAPConnection.LDAP_V3;
         var typesOnly   = false;
  
         var lc = new jldap.LDAPConnection();
  
  	 	 // An ECMAScript array reference cannot
  	 	 // be passed to a Java method that expects
   	 	 // a Java array. So we have to copy our 
  	 	 // 'attribs' array into a legit Java array:      
         var javaStringArray = 
             java.lang.reflect.Array.newInstance( 
  	 	 	 	 	 	 java.lang.String, 
  	 	 	 	 	 	 attribs.length);
         for (var i = 0; i < attribs.length; i++)
             javaStringArray[i] = 
  	 	 	 	 	 new java.lang.String(attribs[i]);
              
  
  	 	 // bind to server
         lc.connect( ldapHost, ldapPort );               
         lc.bind(ldapVersion, loginDN, password );  
  
         	 	 // set up file I/O objects
         var fos = new java.io.FileOutputStream(fileName); 
         var writer = new jldap.util.LDIFWriter(fos);
  
  	 	 // send query
         var results = lc.search( searchBase, 
  	 	 	 scope, 
  	 	 	 searchFilter, 
  	 	 	 javaStringArray, 
  	 	 	 typesOnly);
  
  	 	 // write the results
         while ( results.hasMore() )          
  	     	 	 	 writer.writeEntry( results.next() );
   
  	 	 // clean up                
         writer.finish();   
         fos.close();
         lc.disconnect();   
  
         java.lang.System.out.println("LDIF file written.");
  }

This function is purposely somewhat monolithic in order to show all of the intended functionality in one complete series of related steps. (In the real world, you'd probably factor this function out into two smaller functions: a function that does the bind and issues the query, and another that writes the query result out to an LDIF file.)

The eight arguments represent the standard parameters needed to bind an LDAP directory plus those needed to form a query.

The first line inside the function is:

  var jldap = Packages.com.novell.ldap;

This lets us use a shorthand notation (of jldap) for the longish context string that begins with Packages. (ECMAScript offers access to Java methods through the Packages indirection mechanism.) After we've established this shorthand, we can do

  var lc = new jldap.LDAPConnection();

instead of

  var lc = new Packages.com.novell.ldap.LDAPConnection();

and save ourselves some typing every time we need to get to a JLDAP object.

Once an LDAPConnection object has been instantiated, we can go ahead and bind to the tree:

  	 	 	 lc.connect( ldapHost, ldapPort );               
         	 	 	 lc.bind(ldapVersion, loginDN, password );  

These are standard JLDAP calls, documented in the Novell NDK Javadocs.

Next, we set up our file-I/O objects:

  	 	 	 var fos = new java.io.FileOutputStream(fileName); 
         	 	  var writer = new jldap.util.LDIFWriter(fos);

The file will be written to whatever path was supplied in the fileName argument.

Querying the server requires one line of code:

  	 	 // send query
         var results = lc.search( searchBase, 
  	 	 	  	 	 	 	 	 	 scope, 
  	 	 	 	 	 	 	 	 	 searchFilter, 
  	 	 	 	 	 	 	 	 	 javaStringArray, 
  	 	 	 	 	 	 	 	 	 typesOnly);

The search results can be enumerated and written out very simply:

  while ( results.hasMoreElements() )          
  	     	 writer.writeEntry( results.nextElement() );

Finally, we close all files, streams, and connections:

         writer.finish();   
         fos.close();
         lc.disconnect();   

Testing the Script

To test the script, we can put the following text in a Function Action and run it:

  query2Ldif( 'www.nldap.com'
  	 	   	     'anonymous', 
  	 	   	 	 '', /* no password */
  	 	   	 	 '', /* no search base DN */
  	 	   	 	 0, /* 0 for base-object scope */
  	 	   	 	 '(objectClass=*)', /* filter == all objects */
              new Array('*'), /* attrib array */
  	 	 	     'c:\\temp\\test.ldif' )

When the Function Action with the script runs, it queries the Novell public server for its DSE root information. An LDIF file is written to disk.

The LDIF file can be inspected with a simple text editor. It contains (in part):

  # This LDIF file was generated by the LDIF APIs of Novell's Java LDAP SDK
  version: 1
  
  dn: 
  errors: 21428
  subschemaSubentry: cn=schema
  directoryTreeName: DEVNET-TREE
  securityErrors: 972
  compareOps: 218
  bindSecurityErrors: 850
  outBytes: 2279628812
  vendorVersion: eDirectory v8.7.0 (10410.57)
  simpleAuthBinds: 383333
  supportedFeatures: 1.3.6.1.4.1.4203.1.5.1
  supportedFeatures: 2.16.840.1.113719.1.27.99.1
  repUpdatesIn: 0
  abandonOps: 572
  supportedSASLMechanisms: EXTERNAL
  supportedSASLMechanisms: DIGEST-MD5
  supportedSASLMechanisms: NMAS_LOGIN
  referralsReturned: 0
  removeEntryOps: 6683
  searchOps: 654935
  addEntryOps: 20253
  strongAuthBinds: 32
  modifyEntryOps: 888
  vendorName: Novell, Inc.
  listOps: 0
  modifyRDNOps: 9
  chainings: 300
  ...
  [ etc. ]




Copyright © 2003 Novell, Inc. All rights reserved. Copyright © 1997, 1998, 1999, 2000, 2001, 2002, 2003 SilverStream Software, LLC. All rights reserved.  more ...