Lightweight Directory Access Protocol (LDAP) servers are everywhere in enterprises today, and this article shows you how to access LDAP servers on any platform from RPG.
The IBM i is perhaps the most integrated and integrate-able server platform in the world today. With the exception of a native GUI (which I would argue is not required in a server platform anyway), it's hard to find a modern programming feature that isn't available on the i. Today's article is going to focus on one of those features: Lightweight Directory Access Protocol (LDAP) support. More specifically, it is going to give you a working example program of how to access any LDAP server in your organization from standard RPG.
The Focus of This Article
I won't spend a lot of time explaining LDAP itself; if you're unfamiliar with the term, you can always start at Wikipedia. A nice introductory article can be found here, and an older but even more friendly introduction was written back in 2000 and is still available online. And not surprisingly, probably the most comprehensive document is IBM's Redbook. However, I wanted to point out that LDAP has two distinct sides: the server and the client. From the server side, the IBM i is just as capable as any platform out there. IBM Tivoli Directory Server (ITDS) is easy to set up and manage, and in another article I'll try to give you a primer on getting ITDS started. It's basically a few functions in Systems i Navigator.
But the reality is that today many enterprises use an LDAP server other than the IBM i. That's why today's article is written from the client perspective. As it turns out, the client support on the IBM i is just as extensive as its server support. It's relatively easy to write a simple RPG program to access the data in any LDAP server in your enterprise. I did it in about 150 lines of code, and a lot of that code defined the prototypes for the LDAP programs. And so without further ado, let's take a look at the program.
The LDAP Client Program
I'll walk you through the program in detail.
h option(*nodebugio:*srcstmt) dftactgrp(*no) actgrp(*new)
* External prototypes
d ldap_init pr * extproc('ldap_init')
d host * value options(*string)
d port 10I 0 value
d hLDAP s *
d ldap_search_s pr 10i 0 extproc('ldap_search_s')
d ld * value
d base * value options(*string)
d scope 10I 0 value
d filter * value options(*string)
d attrs * value
d attrsonly 10I 0 value
d res *
d basedn c 'o=My Company'
d filter s 50 varying
d cDescription ds
d 11 inz('description')
d 1 inz(x'00')
d attributes ds
d * inz(%addr(cDescription))
d * inz(*null)
d hResult s *
d rc s 10i 0
d pr * extproc('ldap_first_entry')
d ld * value
d result * value
d hEntry s *
d pr * extproc('ldap_get_values')
d ld * value
d entry * value
d attr * value options(*string)
d ppDescription s *
d pDescription s * based(ppDescription)
d ldap_msgfree pr 10i 0 extproc('ldap_msgfree')
d msg * value
d ldap_memfree pr extproc('ldap_memfree')
d mem * value
d ldap_get_errno pr 10i 0 extproc('ldap_get_errno')
d ld * value
d pr * extproc('ldap_err2string')
d error 10i 0 value
With the exception of a relatively standard H-spec (containing just my typical debugging options and activation group keywords to allow ILE programming), the first part of the program is made up entirely of prototypes for the LDAP APIs. You can find detailed information on the APIs in the IBM Infocenter. The LDAP APIs are a little unusual from an RPG standpoint. They tend to return pointers and even pointers to pointers. This program is very simple, so doesn't get into a lot of the more arcane features of the APIs, but it's enough to get you started.
You'll notice that I use the parameter names from the Infocenter for the parameter names in my prototypes. However, I find that the names in the APIs don't provide much information, so the actual RPG variables are named a bit differently. For example, pointers to normal variables start with a "p," and pointers to pointers start with "pp." Note I said "pointers to normal variables," which implies there is a different type of pointer. That different type of pointer is a handle, and the term denotes a pointer to an application opaque object (an object whose internal structure is unknown to the application). A handle starts with the letter "h." Handles are returned from APIs with the express purpose of being passed to other APIs without being touched or even referenced by the application. So to summarize, a variable starting with "p" points to an RPG variable, while one starting with "h" is intended to be passed only to another API.
* Internal prototypes
dGetDescription pr 3u 0
d stage s 3u 0
dFormatError pr 80
d errorstage 3u 0
These internal prototypes are provided for my friends on back releases. Those of you on a current release can ignore these statements. Lucky you!
* Main prototype
d LDAPTEST pr extpgm('LDAPTEST')
d iUser 10
d iDescription 80
d LDAPTEST pi
d iUser 10
d iDescription 80
This is the primary definition of the program. What does the program do? It provides the description associated with a user name. The sharp-eyed will note that my user name is only 10 characters; that's because my LDAP server is an IBM i, so my user ID is actually my user profile name. But certainly if you were accessing a different server you could provide a longer user ID field. For that matter, your description field could be larger, too, or you could add more attributes. That's the beauty of an example program; you can do whatever you need with it.
stage = GetDescription();
if stage > 0;
iDescription = FormatError(stage);
if ppDescription <> *null;
if hEntry <> *null;
if hResult <> *null;
*inlr = *on;
I noted that this is a very simple program. That doesn't mean it's trivial; this program provides a simple but useful function: it retrieves a single attribute (in this case, the description) from a specific entry. That's only a tiny subset of what LDAP can do, but this is meant to be your first working program.
The mainline has very little LDAP-specific code other than cleanup. But cleanup is important; the LDAP APIs tend to allocate memory, and if your application doesn't free that memory, you can end up with the infamous memory leak—a program that slowly eats more and more memory until serious system errors start to occur. You'll see that I call the getDescription procedure, and then, regardless of the results, I free up all allocated pointers (I assume a pointer is allocated if it is not null).
I'd like to describe for a moment the error processing. The LDAP APIs have their own error processing, which I'll show at the end of the example. But the way this program works is that it executes its logic in stages, and the getDescription functions returns a non-zero stage number if an error occurs. If an error does occur, the mainline calls the formatError procedure to return a human-readable error message instead of the description.
d pi 3u 0
hResult = *null;
hEntry = *null;
ppDescription = *null;
This is the meat of the program. I'm going to review each step of the process. The first thing I do is set all the pointers to null, telling the mainline not to deallocate them. Then I start the actual communication with the LDAP server.
hLDAP = ldap_init('MYLDAPHOST': 389);
if hLDAP = *NULL;
The first step, ldap_init, is standard. You provide the host name and port. The traditional port is 389, and then you supply a resolvable name for your server. You can use *NULL as the host to cause it to use the local host. Why, then, since I'm using the IBM i as my server and my client code is by definition running on the IBM i, don't I use *NULL? Well, since this is an example, I wanted to show you some of the flexibility. By changing the host, I can easily use a different IBM i as my LDAP server—or another machine entirely, including a Linux box or even a Microsoft Active Directory server. All from my little RPG program. Pretty slick, eh?
filter = '(cn=' + %trim(iUser) + ')';
rc = ldap_search_s(
hLDAP: basedn: 2: filter: %addr(attributes): 0: hResult);
if rc <> 0;
Next, the program calls ldap_search_s. The short explanation is that this is a routine that retrieves as many entries as match the filter criteria. My filter is almost criminally simple; it looks for entries that have a common name attribute (cn=) that exactly matches my user ID. It just so happens that this is the way that the IBM i populates the directory. Without going into too much detail, let me just say that the criteria can be very extensive, with all manner of Boolean logic and fuzzy comparisons. This page provides a good introduction to filter syntax. The API also requires an array of pointers to strings to tell the system which attributes to retrieve. My list is again very simple: it is a list with one entry, the attribute named "description."
Other ldap_search APIs exist. The one I selected is synchronous, meaning it waits for the operation to complete but has no timeout, which means it waits forever. Others function differently. The search routine you select depends on your application requirements. Also, since the program never performed a bind operation, it is accessing the directory anonymously. A good overview of LDAP authentication can be found here.
hEntry = ldap_first_entry(hLDAP: hResult);
if hEntry = *null;
The next steps are very simple, in part because my requirement is simple. The ldap_first_entry call returns a handle to the first matching entry. If the handle is null, I return an error.
ppDescription = ldap_get_values(hLDAP: hEntry: cDescription);
if ppDescription = *null;
The final step is to retrieve the description. I call the ldap_get_values API to return a list of all values named "description." You would correctly surmise that an entry could have multiple values for a single attribute. In this case, though, I assume that there is exactly one value; I only bother to get the first value. If there is no value, return an error.
iDescription = %str(pDescription:80);
As I noted, if I successfully retrieve at least one value, I simply set the description parameter to the first retrieved value and return a value of zero, signifying successful completion.
d pi 80
d errorstage 3u 0
d msg s 80 varying
msg = '**ERR: Stage ' + %char(errorstage);
if rc = 0;
rc = ldap_get_errno(hLDAP);
if rc <> 0;
msg += (': ' + %str(ldap_err2string(rc)));
All that's left is the error formatting. Most routines simply signal an error; the application program then calls lda_get_errno to retrieve the error code. Interestingly, the ldap_search routines don't work that way; they return their error code directly. So, the common error routine I've written checks to see if rc already has an error and, if not, calls ldap_get_errno. A subsequent call to ldap_err2string converts the error code into a human-readable string. Append that to the stage number and you have your completed error message.
That's all there is to it. As I noted, this is a very simple example. But it should give you enough to get started with LDAP processing. I particularly like the fact that you can query an Active Directory server from an RPG program. It just proves again the flexibility of my i whenever I can directly access Microsoft objects. Have fun!
as/400, os/400, iseries, system i, i5/os, ibm i, power systems, 6.1, 7.1, V7, V6R1