Novell is now a part of Micro Focus

DeveloperNet University's NDS101 Using C and LDAP

Articles and Tips: article

LAWRENCE V. FISHER
Senior Research EngineerDeveloper Information

01 Dec 1998


This article discusses how to write a C application that uses LDAP to read attribute values from an object in a directory using an anonymous bind.

Introduction

This is another code example in DeveloperNet University's NDS101 category. The information below describes what you will need in order to build its sample application.


Objective
Using this example as a guide, a C/C++ programmer will be able to use LDAP libraries to build a Win32 client-side-only application that can read publicly accessible NDS directory information.

Prerequisites

The student must have entry level C programming skills (other DeveloperNet University examples at this level will have prerequisites unique to their environment).

Required Items

A Win32 development environment.

An LDAP SDK for C and documentation installed. Installs providing LDAP SDKs for C are available free from the following locations: ftp://terminator.rs.itd.umich.edu/ldap/http://developer.netscape.com/tech/directory

Optional Items

Novell's Netware Administrator or ConsoleOne NDS Administration tools are available along with a free DeveloperNet subscription from: http://developer.novell.com/ndk/

ConsoleOne is also available from the standard Netware 5 install CD.

Required Setup

TCP/IP network access to at least one NetWare server running LDAP Services for NDS (available from the standard NetWare 5 install CD).

The tree associated with Novell's LDAP Services NLM must have a publicly accessible object with public attributes in order for this program to read them via an unauthenticated connection (anonymous bind). In this example, attempts to read values protected from public access will fail because this application does not establish the authentication needed to access them. LDAP Authentication issues will be discussed in later examples.

The NDS101 GUI

In order to keep the code and explanations simple, we provide simple GUIs for all examples. Figure 1 shows the GUI provided for NDS101 Using C with LDAP.

Figure 1: The GUI is kept separate from the example code discussed in DeveloperNet University articles.

When the user clicks the "Read Attribute" button, the GUI code will execute the example code to read the attribute specified by "Attribute Name" from the object specified by "Object Name" in the tree associated with the NDS server specified by "Server ID."

Figure 2: You can modify the example code so that the GUI will call your experiments.

Figure 2 shows a code snippet that could be used to perform an NDS read operation. As you can see, there isn't too much code necessary to read a value from NDS using LDAP. However, you need to understand the following basic concepts:

  1. Directory trees.

  2. Directory object context.

  3. Attribute fields and their syntaxes.

The following three subsections discuss these topics. If you are already familiar with directories, skip to source code listing and explanations.

NDS Trees

Note: The following discussion uses Novell Directory Services (NDS) terms to familiarize you with the standard Novell Directory Access Protol (NDAP) terminology. These terms may be different from those used in environments other than NDAP, such as ActiveX or LDAP.

An NDS tree can be viewed as a database containing directory information. The directory's entries are called objects (like database records), which contain information about entities on the network. Figure 3 shows some different types of NDS objects.

Each tree maintains its own distributed NDS database of objects, usually on more than one server. NDS administrators generally use a network administration application provided by Novell to view and help manage their trees. Novell provides an application called NetWare Administrator that can be run from Windows clients for this purpose. Novell also provides a Java application called ConsoleOne, on the server and other platforms, that can be used for NDS administration.

The information in an NDS tree does not describe the physical layout of the network. Instead, an NDS tree view describes arbitrary groups of hardware and software network entities that reflect the organization of the business. The network layout view, on the other hand, usually only depicts the physical connections between the network's hardware entities.

Figure 3: An NDS tree shows how the administrator wants to look at the entities on the network.

NDS Object Context

The object type an administrator selects when creating an object to represent a network entity depends on the purpose of the entity. Each object contains the specific information needed for it to perform its role in the network. There are many standard object types provided by Novell and often it is necessary for applications to add a new object type for special uses.

User objects contain information about the people who operate the client stations on the network. In NetWare Administrator and ConsoleOne, an object can be double-clicked to display its information. This information is read from the object's attributes. NDS attributes are discussed later.

Tree administrators arbitrarily create, edit, and delete objects on a tree as they determine it necessary. If you have never used NetWare Administrator or ConsoleOne to perform these tasks and have admin rights on a tree, now would be a good time to log in and try them before continuing with this article.

Organization objects like the Developer Org object in Figure 3 are often at the highest level in the tree. There can be multiple Organization objects but this tree has only one. Organizations and Organizational units such as the Tech Info and Education objects in Figure 3 are called containers. Organizational Units are the mid-level containers in the tree.

Administrators nest containers to represent the structure within the organization. Organizations and Organizational Units have no physical presence on the network. You can see that there are no entities in the network layout which correspond to the Organization or Organizational Unit objects. They are simply conceptual NDS devices designed to help organize directory information.

Administrators generally create user objects inside of Organizational Unit objects representing the groups that the users work in. The path of Organization and Organizational Units leading to an object is actually a part of its name, like a fully qualified pathname in a file system.

An object's fully qualified pathname in NDS is called its NDS name context. Figure 4 shows the different kinds of name contexts that an object can have in an NDS tree.

Figure 4: Objects can be named by the paths leading to their location in the tree.

The first two name contexts listed in Figure 4 are referred to as distinguished name contexts because, as complete paths, they can distinguish between the Joe in Accounting and the Joe in Marketing. If the types for each step along the way are specified, the context is also referred to as typeful (CN refers to a leaf object's "Common Name").

A relative name context requires the accessing station's current context be set so that it can complete a partial reference in order to fully distinguish a target object. For example, in the relative context example in Figure 4, current context would need to be set to "Accounts_Receivable.Accounting.Novell".

NDS Attributes

Objects are composed of attributes. Before an object can be created, the tree must possess knowledge of the attributes that should go into the object's type. These object type descriptions are called classes. Like a recipe, an NDS class describes the composition of the objects created from it. For those of you familiar with object-oriented programming, classes in NDS are very analogous to classes in C++ or Java, in that you instantiate NDS objects from their NDS class definitions. Figure 5 shows a partial list of the attributes in a User object.

Figure 5: A user object has many more attributes than those shown here.

When an object is created, it must be initialized with all of its mandatory attributes and have values for them. However, an object may or may not have any of its optional attributes. As you would imagine, attempting to read an optional attribute value from an object which doesn't have that attribute yet will fail.

Attributes are based upon Syntaxes. Syntaxes are special typedefs created by Novell to be the data types used in attribute definitions. Every attribute definition is built upon a syntax. The syntax specified for an attribute definition defines the data format for values that can be put into that attribute type. You need to know an attribute's syntax in order to anticipate the type of value that you will receive back from a read operation.

New LDAP Terms

For the most part, standard NDS terms work for LDAP. However, there are two new terms and some subtle differences in others.


NDS Term
Corresponding LDAP Term

Connection

Bind

Object

Entry

NDS uses the term connection to refer to the information framework set up by the client and an NDS server on the network that allows them to communicate and also for the level of authenticated access (as in an "authenticated connection"). LDAP also uses the term connection to refer to the information framework set up by the client and a directory server but uses the term bind to describe the level of directory access (as in "anonymous bind").

NDS uses the term object to refer to the equivalent of a record in an NDS tree (see "NDS Trees" above). LDAP uses the term entry to refer to the same thing.

Setting Up to Run this Example

Figure 6: NDAP client applications can share connections; however, each LDAP application must establish its own connections.

As you can see in Figure 6, calls made to NDS from applications built with the regular NetWare C client SDK, using the Novell Directory Access Protocol (or NDAP), go through the NetWare DLLs that were installed with the NetWare client.

NetWare's client DLLs maintain connection information in tables available to any application running on the platform. This means that the applications can all share connections, removing the necessity for each of them to log in separately. The NDAP NDS101 example is such an application. This is why it doesn't require code to log in. Instead, it shares the authenticated connection established during the initial user login.

LDAP client applications cannot share connection information. They must each connect and authenticate separately.

LDAP applications connect using IP. With older versions of LDAP, an authenticated bind can be achieved by passing the name and password in clear text over the connection. Version 3 of LDAP enables the use of a Secure Sockets Layer (SSL) connection where the name and password are encrypted.

Note: Currently, unless the "Allow Clear Text Passwords" selection is checked in the LDAP Group object (accessible through Netware Administrator and available soon in ConsoleOne) for the target tree, the LDAP Services for NDS NLM (the service which manages LDAP access to NDS directories) will reject any attempt to authenticate over a non-SSL connection and default to an unauthenticated bind.

Since the NDS101 example series is concerned exclusively with reading attributes from objects in an NDS directory, this example will only establish an unauthenticated bind to read attributes from the directory and will leave out the SSL connection and authentication code.

Note: Obtaining authenticated directory access using LDAP will be discussed in a later DeveloperNet University example.

Because this example will be using unauthenticated access, it will only be able to read publicly accessible information from the directory. If you have administrator rights on a test tree, now would be a good time to start ConsoleOne and set up a target object on the target tree for public access. Here's how:

First, select the object that you will be reading attributes from and then display its trustee information as shown in Figure 7.

Figure 7: Select the target object and then display its trustees.

Next, select [Public] and click "Assigned Rights" to display the rights that have been assigned to [Public], as shown in Figure 8.

Figure 8: Display the assigned access rights of [Public] for the target object.

Finally, as shown in Figure 9, select Browse object rights and at least the "Read" selected property right on the attribute that you will be reading with this example application.

Figure 9: Allow [Public] at least Read and Compare access to the target object.

The value or values of the attribute that you set up for public "Read" access will now be obtainable using this example's LDAP application, as shown in Figure 10.

Figure 10: The NDS101 LDAP GUI will call your experiment when the "Read the Attribute Value" button is selected.

Finally, the Source Code Listing

Below is a source code listing for NDS101 using LDAP with C. All LDAP data types and functions are in bold. The large, underlined numbers refer to the explanations after the code listing.

There are several LDAP SDKs available free. This example uses Netscape's nsldapssl32V30 library. Since it is LDAP, it can be used with any LDAP-accessible directory including NDS.

You can install Netscape's Libraries for LDAP from the Web: http://netscape.com/tech/directory/

Once you have installed the LDAP SDK, make sure that you add the appropriate path names to your development environment so that it can access the headers and library before you attempt to build this sample application.

1   LPSTR serverName   = "Deved3";// could use the server's IP address here;

    LPSTR objectName   = "cn=admin, o=Novell";

    LPSTR attributeName= "cn";  //cn, givenname, sn, telephonenumber, etc.





    void __declspec(dllexport) WINAPI  getFieldStringValue(LPSTR theBuffer)

    { LDAP         *connInfo = NULL;

      LDAPMessage  *resultBfr = NULL,*entry = NULL;

      BerElement   *berEl;

      char         *rcvdAttrNam;

      char         **vals;

      int          valIndex;

      char*        attrs[2];

2   if ( (   connInfo = ldap_init( serverName, LDAP_PORT ) ) == NULL )

                (void)lstrcat( theBuffer, "ldap_init error" );

    else

3   { if ( ldap_simple_bind_s(   connInfo, NULL, NULL) != LDAP_SUCCESS )

         displayError(   connInfo, theBuffer, resultBfr );

      else

4   { attrs[0]= attributeName;  attrs[1]= NULL;

      if( ldap_search_s( connInfo, objectName, LDAP_SCOPE_BASE,

                      "(objectclass=*)",attrs,0,&resultBfr) != LDAP_SUCCESS)&
       displayError(   connInfo, theBuffer, resultBfr );

      else

5   { for(  entry = ldap_first_entry(   connInfo, resultBfr);

              entry!= NULL;

              entry = ldap_next_entry(   connInfo, entry ) )

        {

          for(     rcvdAttrNam = ldap_first_attribute(  connInfo, entry, &berEl ); &
                   rcvdAttrNam != NULL; 

                   rcvdAttrNam = ldap_next_attribute(   connInfo, entry, berEl) )

          { if(( vals = ldap_get_values(   connInfo, entry, rcvdAttrNam )) != NULL)

          {

            for( valIndex = 0 ; vals[valIndex] != NULL; valIndex++ )

              (void)lstrcat( theBuffer, vals[valIndex] );

            ldap_value_free( vals );

           }

           ldap_memfree( rcvdAttrNam );

          }

          if ( berEl != NULL )       ber_free(berEl,0);

         }

        }

       }

       freeReadResources (    connInfo, resultBfr );

      }

     }

6      void   freeReadResources (  LDAP *  connInfo, LDAPMessage *resultBfr )

       { if (resultBfr != NULL)  {  ldap_msgfree( resultBfr);    }

         if (  connInfo != NULL)     {  ldap_unbind(  connInfo);    }

       }

7   void displayError(   LDAP *   connInfo, LPSTR theBuffer, LDAPMessage *resultBfr )

    { char  *firstGoodDN,*errMsg,tempstr[512]="";     int    errID;



      errID = ldap_get_lderrno(   connInfo, &firstGoodDN, &errMsg );&
      lstrcat(tempstr,errMsg);

      if( errID == LDAP_NO_SUCH_OBJECT)

      { lstrcat(tempstr,"/nFirst existing object in dName = ");

        lstrcat(tempstr,firstGoodDN);

      }

      lstrcat(theBuffer, tempStr);

      freeReadResources (    connInfo, resultBfr );

     }
  1. Enter the domain name or IP address for the server running Nldap.nlm into serverName. Enter the domain name or IP address for the server running the LDAP Services for NDS NLM, Nldap.nlm. As shown in Figure 11, Nldap.nlm moderates access to the NDS tree associated with its server. If you use the Netscape LDAP library, your LDAP application must have access to the appropriate Netscape LDAP DLL on the client (listed in the Path or located in the same directory as the experiment DLL).

    Figure 11: Nldap.nlm moderates access to the NDS tree associated with its server.

    Enter the distinguished name for the object to be read. Notice that LDAP delimits the objects and containers in the distinguished name path with ',' instead Novell's name delimiter character '.'

    Enter the selected attribute into attributeName. For our sample code, we need to choose an attribute that we know will always exist in the object, like a mandatory attribute. Also, because our GUI interface expects a String, we need an attribute with a syntax that can be viewed as a String. Some NDS syntaxes have no human readable String value.

    The user object's "sn" (the LDAP equivalent of the NDS attribute surname) attribute is ideal for our purposes because it is mandatory so it will always exist in every user object and it uses the "Case Ignore String" syntax. Enter the common name attribute's name "cn" as the attributeName value in the example source code.

    Note: Many of LDAP's attribute names have the same meaning but are different from Novell's. For example, Novell uses "Telephone Number" with a space, while LDAP refers to "telephonenumber" with no space. If the correct attribute name selection is made at the LDAP client end, the Novell LDAP service will make sure that the appropriate attribute is read from the NDS tree. For more information see "Configuring LDAP Services for NDS" in the November issue of Developer Notes.

  2. The ldap_init( ) call returns a pointer to an LDAP data structure which describes the obtained connection to the specified LDAP server. We don't show the members of the LDAP data structure here because it is not intended to be directly accessed by programmers. Instead LDAP programmers are to use the accessor routines provided in the LDAP libraries.

  3. By passing NULL as the object name and NULL for the password in the ldap_simple_bind_s( ) call, we tell LDAP that we want unauthenticated access on the specified connection.

    The "_s" in the ldap_simple_bind_s( ) call specifies that it is a synchronous call (it will block the current thread of execution until the operation is complete). Many LDAP calls, including this one, have both synchronous and asynchronous versions. We use the synchronous versions in our examples, because they are easier to explain.

  4. The ldap_search_s( ) is the synchronous version of the call that performs the real work in the read operation. Below are the parameters for ldap_search_s( ).

    int ldap_search_s ( LDAP *connInfo,
    
          const char         *base, 
    
          int                scope, 
    
          const char         *filter, 
    
          char               **attrs,   
    
          int                attrsonly, 
    
          LDAPMessage        **resultBfr   );

    LDAP *connInfo. A pointer to an LDAP structure containing the necessary connection information between the LDAP client and the LDAP server. This structure is intended to be inaccessible to LDAP programmers. LDAP provides accessor calls in its libraries to perform necessary getting and setting of info.

    char *base. A string contains the distinguished name describing the location in the directory tree where the search should begin.

    int scope. The scope parameter tells LDAP where to begin the search in relation to the base as defined by the following constants:


    *LDAP_SCOPE_BASE

    searches only the entry specified by base

    *LDAP_SCOPE_ONELEVEL

    searches all entries one level beneath the entry specified by base

    *LDAP_SCOPE_SUBTREE

    searches all entries at all levels beneath the entry specified by base

    This example uses LDAP_SCOPE_BASE because it is only interested in the object to be read.

    char *filter. LDAP doesn't perform a pure read operation as does the Novell Directory Access Protocol. Instead, a read is accomplished as a form of an LDAP search operation.

    LDAP search operations are much like NDAP search operations in that they require a search filter expression to define the object that is being looked for. In this case, the asterisk wildcard character in the expression "(objectclass=*)" specifies that all objects within the context established with "base" and "scope" are targets.

    In our example, since the scope parameter LDAP_SCOPE_BASE specifies that only the object specified by the base attribute is to be accessed, only the object described by the program's objectName variable will be selected in the search, no matter what kind of object that may be.

    char* attrs[ ]. A NULL-terminated array of strings specifying the attribute types that are to be read from the target object or objects. A NULL here would return all values in the selected object.

    In the case of our example, only the attribute selected for the attributeName variable is to be read from the selected object.

    int attrsonly. A zero in this parameter specifies that we want the values of the specified attributes as well as their names returned.

    In the attrsonly parameter, a 1 would specify that only the existing attribute names were wanted. This would be useful for checking to see if an object contained a specific optional attribute.

    LDAPMessage resultBfr. The LDAPMessage is another structure whose members are intended to be inaccessible to LDAP programmers. An LDAPMessage can contain a lot of different things. The LDAPMessage result returned by ldap_search_s( ) will contain the complete results of the search operation. If the search found several objects, the result buffer would contain the read values for all of the specified attributes in all of those objects.

    In this example, a successful operation will result in a buffer with only one attribute from the specified object.

  5. Like the Novell Directory Access Protocol (NDAP), retrieving information from LDAP buffers must follow a specific formula and the form used to retrieve information is usually achieved with nested loops. In this example, there are three nested loops.

    The first loop retrieves an LDAPMessage containing information for each entry (NDS object) in the buffer.

    The second sequences through the attributes in an entry to retrieve its values. The BerElement (Basic Encoding Rules element) variable is used internally by LDAP to keep track of the attributes in the entry. The second loop first gets the name of the current attribute in the entry with the ldap_first_attribute( ) and ldap_next_attribute( ) calls. The name is necessary to determine what kind of attribute has been retrieved because the attributes can be in any order in the entry. Then, the second loop obtains the attribute's value with the ldap_get_values( ) call, specifying the attribute name.

    The third nested loop may not really be necessary for this example because we only want one value; however, there could be multiple values for the attribute and we should follow the recipe.

  6. The application is responsible for freeing memory and other resources allocated by LDAP during its operation.

  7. ldap_get_lderrno( ) returns the last error that occurred on the connection specified by connInfo. In every case, the errMsg string will contain an explanation. If the errID equals LDAP_NO_SUCH_OBJECT, firstGoodDN will be the good portion of the name context parameter (if any) passed to the operation causing the error. This tells the programmer at what point the distinguished name context went awry.

Conclusion

After reading this article, you have learned about NDS trees, objects, contexts, attributes, and NDS syntaxes. If you then built and ran the lab, you also learned how to write a C application that uses LDAP to read attribute values from an object in a directory using an anonymous bind (as long the object and attribute are publicly accessible).

After this brief introduction, we hope that you have discovered that NDS programming really isn't difficult.

As you experiment with the code example, try rebuilding it to read different attribute names.

Note: Other NDS101 examples present additional ways to develop to NDS, like ActiveX.

For more information about other examples in this series, refer to: http://developer.novell.com/education/

To download the project corresponding to this article, refer to: http://www.novell.com/coolsolutions/tools/15625.html

There is an LDAP Services for NDS test server available for your programming experiments on the usual LDAP port 389 at: http://www.nldap.com

You might also try the below URL to run an interesting LDAP demo with your browser: http://ldap.novell.com/

* 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