Novell is now a part of Micro Focus

Using JNDI and Novell's NJCL to Access NDS

Articles and Tips: article

ANN DAVIS
Software Engineer
DeveloperNet Labs

01 Oct 1999


Discusses how to use JNDI/NJCL to perform several basic NDS administrative tasks: logging in to the NDS tree, searching the NDS tree, adding a user to the tree, modifying attributes for a user, deleting a user, and logging out of the NDS tree. Includes sample code for each of these tasks.

Introduction

If you are a developer who is looking for a good Java way to access and administer NDS, the Java Naming and Directory Interface (JNDI) combined with Novell's JNDI NDS Provider is an excellent choice. JNDI is a directory-independent standard supplied by Sun Microsystems to allow programmatic directory access to any directory with JNDI providers. Novell's JNDI NDS provider is included in Novell's Java Class Libraries (NJCL) and offers classes and methods that can be used to implement and extend the functionality of JNDI to perform NDS-specific tasks. NJCL includes several other NetWare-specific JNDI providers along with the NDS provider; however, in this Developer Note we'll discuss only the JNDI NDS provider. Any references to NJCL refer only to the JNDI NDS provider portion of NJCL.

There are a couple of key points to understand before you create your JNDI/NJCL programs. One point stems from a question many developers have asked: "Can I write a JNDI-only program to access NDS?" The answer is "yes," depending on what task you are attempting. Tasks that are considered "directory-independent," such as listing or searching for objects, can be done with JNDI alone. However, if you wish to reference any NDS-specific class names or attributes, then you will need to use the JNDI NDS provider included in NJCL.

Another important point to understand is that if you decide to run a JNDI/NJCL program on a client system (rather than a NetWare server), you need a NetWare client installed on that client system.

Note: This is not the case if you are using the JNDI LDAP provider.

All of Novell's current JNDI providers (except LDAP) use the NetWare client libraries to perform their tasks. This will not be the case in the future; in fact, there is already an RMI, non-client-dependent version of NJCL available pre-release from http://developer.novell.com/ndk under the Leading Edge link.

With these points in mind, we will discuss how to use JNDI/NJCL to perform several basic NDS administrative tasks: logging in to the NDS tree, searching the NDS tree, adding a user to the tree, modifying attributes for a user, deleting a user, and logging out of the NDS tree. This Developer Note covers each of these tasks and the sample code associated with them.

Note: The import statements and encompassing class definition are not included in the following code snippets. They are included in the full sample provided at the end of this article.

Authenticating

By itself, JNDI does not provide any authentication capabilities, so logging in to an NDS tree is performed using only NJCL classes and methods.

Notes: Many operations do not require having an authenticated connection to the NDS tree; however, the operations our program will perform do require authentication.

NJCL authentication is performed using existing NetWare authentication strategies. When an NJCL program runs on a NetWare client system, it uses the Novell Client's authentication to establish, or provide, a secure connection to the NDS tree. When an NJCL program runs on a NetWare server, the authentication methods use the Client Requester to establish a secure connection. This authentication strategy implies two important points. First, any client system where you want to run your program needs to have a NetWare Client installed, and then your program will automatically receive all the rights provided by the Novell Client. In other words, if your client is already authenticated to the NDS tree as a specific user, there is no reason for your program to re-authenticate unless you wish it to run as a different user. Second, in the server case, the NetWare Server console is not assumed to be an authenticated connection. Therefore, anytime you run your program on a NetWare server it will have to explicitly authenticate.

When you actually use the Authenticator.login() method, you can choose to have your program display a GUI screen where a user can enter his name, context, and password, or you can choose to have your program do a completely programmatic, non-interactive login. Our program will do a GUI login that is discussed in further detail below.

Below is the part of our program that performs the login task:

private static void login()

{

  System.out.println("**** Logging in ****");

  try

  {

    NdsIdentityScope idenScope = null;

    idenScope = new NdsIdentityScope(adminContext, new NdsIdentityScope(tree,

                                                         new NdsIdentityScope()));

    identity = new NdsIdentity(adminUser, idenScope);

    Authenticator.login(identity);

  }

  catch(Exception e)

  {

    System.out.println("login: Exception thrown: " + e.getMessage());

    e.printStackTrace();

    System.exit(-1);

  }

The first line in the "try" defines an instance of the NdsIdentityScope class that will hold the scope, or context, of where the login object exists. As shown by the second line inside the try, an NdsIdentityScope object is actually constructed by "chaining together" other NdsIdentityScope objects. Depending on how deep in your NDS tree you are, you have to step up through the successive containers until you reach the top level, which is the tree name. In our example, the login object exists in the top-level organization of the tree, thus we only have to chain together two calls to the NdsIdentityScope constructor.

Note: The login.java sample code available on the NDK contains a recursive method for creating the NdsIdentityScope. This recursive method allows you to log in as an object that exists anywhere in your tree, regardless of depth.

The third line in the try creates an NdsIdentity based on the login name and the scope. Once we have created an NdsIdentity, we can call the Authenticator.login method to authenticate that identity to the tree. This is the point at which you would make a modification in order to perform a non-GUI login. To perform a non-GUI login, you would need to create an NdsPasswordIdentity rather than an NdsIdentity to be passed to the login method. An NdsPasswordIdentity is simply an NdsIdentity that includes a password in its private data.

Searching

Searching a directory tree is probably one of the most often-performed tasks, and much of the search methodology is directory-independent. For this reason, we can use the JNDI classes and methods for our search. The only point at which any NDS-Provider information comes into our search is when we set up the filter based on an NDS-specific object.

Note: In many cases, it is not necessary to perform a search. A lookup works just as well. The DirContext.lookup() method searches for a specific object in a specific context. However, if you wish to be able to search on other filters, such as specific attribute values, you do have to use DirContext.search(). In this example, we use the search() method rather than the lookup() method simply to demonstrate how searching is done.

Here is the portion of our code that is responsible for the search:

private static void search()

{

  System.out.println("**** Searching ****");

  // Set the initial context factory to NDSInitialContextFactory

  Hashtable systemProps = new Hashtable();

  systemProps.put(

    Context.INITIAL_CONTEXT_FACTORY,

    Environment.NDS_INITIAL_CONTEXT_FACTORY);

  systemProps.put(Context.PROVIDER_URL, tree);



  try

  {

    // Set the initial context

    ctx = new InitialDirContext(systemProps);



    // See if the user already exists in the user context

    NamingEnumeration sEnum = null;

    // Set up filter

    String filter = "(CN=userName)";

    // Set up constraints to do single-scope search

    SearchControls constraints = new SearchControls();

    constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);

    // Search the user context for the name

    sEnum = ctx.search(userContext, filter, constraints);



    int count = 0;

    if (sEnum.hasMore())

    {

      System.out.println("Name already exists; choose another name.");

      System.exit(-1);

    }

  }

  catch (javax.naming.NamingException e)

  {

    System.out.println("search: Exception thrown: " + e.getMessage());

    e.printStackTrace();

    System.exit(-1);

  }

}

In this sample code, the first thing we do is set up our context. Just because we have logged in to the tree does not mean that we have established a DirContext from which to perform any tasks. We define our context by specifying that we are working with NDS and giving the name of our tree. We must set up a context even after logging in because a client may be logged in to more than one tree at a time, and authenticating does not automatically create a DirContext for you. So, it is in the first line of the try that we actually establish our initial context as the root of the tree we specified. This line would throw an exception if the specified context could not be found.

The next portion of the try is where we set up the parameters for our search. We are searching a specific container in our tree to determine if a specific user (the one we will later create) already exists there. So, for the filter, we specify our user name. Then, for the constraints, we specify that we want to do a one-level search because we are only interested in knowing if our user already exists in a specific container. We don't care if it already exists in some other place in the tree. Once we have set up the filter and the constraints, we can perform the search, passing in the specific container (relative to our DirContext) that we want to search, and getting as a result an enumeration of SearchResult objects. It's at this point that we step through the Search Result objects to determine if there are any. If an object of our user name does already exist, we print a message and exit the program.

Creating a User

Creating a user (or any object) in a directory is necessarily a directory-specific operation. Different directories contain different types of objects, and specifying what type of object we wish to create requires using terms specific to our directory. In our case, with NDS, we want to create a user object. Thus, part of the code for creating a user will be JNDI code and part will make use of the JNDI NDS provider classes and methods.

Below is the portion of our code that creates a user in NDS:

private static void createuser()

{

  System.out.println("**** Create new user ****");

  try

  {

    Attributes attrs = new BasicAttributes();

    attrs.put("Object Class", new NdsCaseIgnoreString("User"));

    attrs.put("Surname", new NdsCaseIgnoreString(surname));



    ctx.createSubcontext(userName + "." + userContext, attrs);

  }

  catch (Exception e)

  {

    System.out.println("createuser: Exception thrown: " + e.getMessage());

    e.printStackTrace();

    System.exit(-1);

  }

}

The first line in the try block creates a new instance of the BasicAttributes class, and the next two lines place attribute name - attribute value pairs into this memory. It is important to note that mandatory attributes must be specified before creating a user. Object Class and Surname are the mandatory attributes for an NDS User. (Information on mandatory and optional attributes for different NDS object types can be found in Novell's NDS Schema Specification.) It is also important to note that when the values for the Object Class and Surname attributes are specified, new instances of the class NdsCaseIgnoreString are used. Novell's JNDI NDS provider does not consider the values of NDS attributes to be simple strings. In this case, our values are of type NdsCaseIgnoreString. Values of other attributes may be of other types. This information is provided in the Novell's NJCL documentation for the JNDI NDS Provider.

Once we have set up values for the mandatory attributes, we can actually tell NDS to create our new user. This is done with the "createSubcontext()" method of DirContext. We pass in as our first parameter the relative distinguished name of our user (relative to our current context) and as our second parameter the attributes to be associated with our new user. This line will throw an exception if for some reason the new user cannot be created.

Modifying Attribute Values

After we have created a new user, we might wish to modify the values of certain attributes of the user or add values for currently unspecified attributes. This is done the same way we specified the mandatory attributes for our new user in the previous code.

The code for modifying attribute values for our user is shown below:

private static void modattrs()

{

  System.out.println("**** Modifying attributes ****");

  try

  {

    // Create the initial ocntext

    String name = userName + "." + userContext;

    Object nameObj = ctx.lookup(name);    // check that we can get the object 



    ModificationItem mods[] = new ModificationItem[2];



    // Put in surname

    Attribute mod0 = new BasicAttribute("Surname", new NdsCaseIgnoreString(surname));

    mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);

    // Put in telephone number

    Attribute mod1 = new BasicAttribute("Telephe Number", new NdsTelephoneNumber(telNumber));

    mods[1] = new ModificationItem(DiurContext.ADD_ATTRIBUTE, mod1);

    ctx.modifyAttributes(name, mods);

  }

  catch (Exception e)

  {

    System.out.println("modattrs: Exception thrown: " + e.getMessage());

    e.printStackTrace();

    System.exit(-1);

  }

}

The first non-comment line in the try sets up the complete relative distinguished name of the user for which we wish to modify attribute values. The next line performs a lookup on this object as a way of checking that we can access the object we have specified.

Next, we set up an instance of the ModificationItem class with space for 2 modifications, or space for modification of 2 attributes. Each element of a ModificationItem array contains two things: a specification of what type of modification we wish to do and the new values relating to that modification. The new values are stored in an instance of the BasicAttribute class, as shown when we set up an instance of the BasicAttribute class for our new value of the surname attribute. After we set up our Basic Attribute instance, we can construct the first element in our ModificationItem array by specifying as the first parameter that we are replacing the attribute in question, and as the second parameter BasicAttribute containing the replacement value. Setting up the second element of the ModificationItem array follows the same procedure, except for two things. First, our new telephone number is an instance of NdsTelephoneNumber, not simply a string or even an NdsCaseIgnoreString. Second, the first parameter of our ModificationItem array element is specified to be an ADD_ATTRIBUTE since there is no existing value for the telephone number attribute of this object.

Once we have set up the memory which specifies the modifications we wish to make, we use the modifyAttributes() method of DirContext to tell NDS to make the modifications, provided we can access our object and have set up our array correctly. If there is any problem in performing the modifications, this line will throw an exception.

Deleting a User

Now that we have created and modified attributes for a new user in our tree, we delete that user in order to return the tree to its original state. The following code shows how to delete a user:

private static void deleteuser()

{

  System.out.println("**** Deleting new user ****");

  try

  {

    ctx.destroySubcontext(userName + "." + userContext);

  }

  catch (Exception e)

  {

    System.out.println("deleteuser: Exception thrown: " + e.getMessage());

    e.printStackTrace();

    System.exit(-1);

  }

}

All that is needed to delete a user is to call the destroySubcontext() method of DirContext with the complete relative distinguished name (relative to our current context) passed in as a parameter.

Logging Out

Now that we have returned the tree to its original state, we log out of the tree.

Note: If this program is running on a client, the connection to the tree for this specific user will be destroyed.

The code to log out of the tree is shown below:

private static void logout()

{

    System.out.println("**** Logging out ****");

  try

  {

    Authenticator.logout(identity);

  }

  catch (Exception e)

  {

    System.out.println("logout: Exception thrown: " + e.getMessage());

    e.printStackTrace();

    System.exit(-1);

  }

}

As with the login, the logout displays a GUI dialog box that requires that you click on the OK button to complete the logout. If the identity passed in to the logout() method is an NdsPasswordIdentity rather than an NdsIdentity, the logout will be performed without showing a GUI and without questioning the user for verification.

Summary

This program has gone through several simple procedures that are often used, in some form, in administering NDS. These procedures can be programmed using other languages and API sets, but using Java with JNDI and NJCL provides an easy, platform-independent method with a great deal of flexibility. Java provides the platform independence, and the standard nature of JNDI provides you with the ability to change underlying JNDI providers (to LDAP, for instance) without requiring major modifications to your code. JNDI also allows you to easily expand your existing JNDI code to use other providers to perform other tasks, such as file system or queue management operations. JNDI, combined with NJCL, offers a rich set of capabilities to Novell's Java developers.

Complete Code Listing:

//

// Copyright (c) 1999 Novell, Inc. All Rights Reserved.

//

// THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND TREATIES.

// USE AND REDISTRIBUTION OF THIS WORK IS SUBJECT TO THE LICENSE AGREEMENT

// ACCOMPANYING THE SOFTWARE DEVELOPMENT KIT (SDK).  NOVELL HEREBY GRANTS 

// TO  DEVELOPER A ROYALTY-FREE, NON-EXCLUSIVE LICENSE TO INCLUDE NOVELL'S 

// SAMPLE CODE IN ITS PRODUCT.  NOVELL GRANTS DEVELOPER WORLDWIDE

// DISTRIBUTION RIGHTS TO MARKET, DISTRIBUTE, OR SELL NOVELL'S SAMPLE CODE AS

// A COMPONENT OF DEVELOPER'S PRODUCTS.  NOVELL SHALL HAVE NO OBLIGATIONS

// TO DEVELOPER OR DEVELOPER'S CUSTOMERS WITH RESPECT TO THIS CODE.

//



import java.util.Hashtable;



import javax.naming.*;



import javax.naming.directory.*;



import com.novell.java.security.Authenticator;

import com.novell.java.security.Identity;

import com.novell.java.security.IdentityScope;

import com.novell.service.security.NdsIdentity;

import com.novell.service.security.NdsIdentityScope;

import com.novell.service.nds.*;

import com.novell.utility.naming.Environment;





public class NdsProg

{

  static String tree = "MB";

  static String adminUser = "dcb111user";

  static String adminContext = "MB";

  static String userContext = "dcb111.MB";

  static String userName = "anja";   // Place your name here

  static String surname = "davis";    // Place your surname here

  static String telNumber = "123-4567";  // Place your telephone number here

  static DirContext ctx;

  static Identity identity = null;



  //

  //  main

  //

  public static void main(String args[])

  {

    try

    {

      ndsProg ndsInst = new NdsProg();

      ndsInst.login();

      ndsInst.search();

      ndsInst.createuser();

      ndsInst.modattrs();

      ndsInst.deleteuser();

      ndsInst.logout();

      System.exit(0);

    }

    catch (Exception e)

    {
System.out.println("main: Exception thrown: " + e.getMessage());

    }

  }

   

  //

  //  Login

  //

  private static void login()

  {

    System.out.println("**** Logging in ****");

    try

    {

      NdsIdentityScope idenScope = null;

      idenScope = new NdsIdentityScope(adminContext, 

            new NdsIdentityScope(tree, new NdsIdentityScope()));

      identity = new NdsIdentity(adminUser, idenScope);

      Authenticator.login(identity);

    }

    catch(Exception e)

    {

      System.out.println("login: Exception thrown: " + e.getMessage());

      e.printStackTrace();

      System.exit(-1);

    }

  }

  

  //

  //  Search

  //

  private static void search()

  {

    System.out.println("**** Searching ****");

    // Set the initial context factory to NDSInitialContextFactory

    Hashtable systemProps = new Hashtable();

    systemProps.put(

         Context.INITIAL_CONTEXT_FACTORY,

         Environment.NDS_INITIAL_CONTEXT_FACTORY);

    systemProps.put(Context.PROVIDER_URL, tree);



    try

    {

      // Set the initial context

      ctx = new InitialDirContext(systemProps);



      // See if the user already exists in the user context

      NamingEnumeration sEnum = null;

      // Set up filter

      String filter = "(CN=" + userName + ")";

      // Set up constraints to do single-scope search

      SearchControls constraints = new SearchControls();

      constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);

      // Search the user context for the name

      sEnum = ctx.search(userContext, filter, constraints);



      int count = 0;

      if (sEnum.hasMore())

      {

        System.out.println("Name already exists; choose another name.");

        System.exit(-1);

      }

    }
catch (javax.naming.NamingException e)

    {

      System.out.println("search: Exception thrown: " + e.getMessage());

      e.printStackTrace();

      System.exit(-1);

    }

  }



  //

  //  createuser

  //

  private static void createuser()

  {

    System.out.println("**** Creating new user ****");

    try

    {

      Attributes attrs = new BasicAttributes();

      attrs.put("Object Class", new NdsCaseIgnoreString("User"));

      attrs.put("Surname", new NdsCaseIgnoreString(surname));

         

      ctx.createSubcontext(userName + "." + userContext, attrs);

    }

    catch (Exception e)

    {

      System.out.println("createuser: Exception thrown: " + e.getMessage());

      e.printStackTrace();

      System.exit(-1);

    }

  }



  //

  //  modattrs

  //

  private static void modattrs()

  {

    System.out.println("**** Modifying attributes ****");

    try

    {

      // Create the initial context

      String name = userName + "." + userContext;

      Object nameObj = ctx.lookup(name);            

      // check that we can get the object



      ModificationItem mods[] = new ModificationItem[2];



      // Put in surname

      Attribute mod0 = new BasicAttribute("Surname", new

            NdsCaseIgnoreString(surname));

      mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);

      // Put in telephone number

      Attribute mod1 = new BasicAttribute("Telephone Number", new 

            NdsTelephoneNumber(telNumber));

      mods[1] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod1);

      ctx.modifyAttributes(name, mods);

    }

    catch (Exception e)

    {

      System.out.println("modattrs: Exception thrown: " + e.getMessage());

      e.printStackTrace();

      System.exit(-1);

    }

  }
//

  //  logout

  //

  private static void logout()

  {

    System.out.println("**** Logging out ****");

    try

    {

      Authenticator.logout(identity);

    }

    catch (Exception e)

    {

      System.out.println("logout: Exception thrown: " + e.getMessage());

      e.printStackTrace();

      System.exit(-1);

    }

  }

}

* Originally published in Novell AppNotes


Disclaimer

The origin of this information may be internal or external to Novell. While Novell makes all reasonable efforts to verify this information, Novell does not make explicit or implied claims to its validity.

© Copyright Micro Focus or one of its affiliates