Приглашаем посетить
Чулков (chulkov.lit-info.ru)

A Sample LDAP Application in PHP

Table of Contents
Previous Next

A Sample LDAP Application in PHP

So we finally get down to putting to some practical purpose what we have gleaned through the course of this chapter.

We will develop an application that will export the directory information for the employees of our favorite company Foo Widgets Inc. Let us look at what could be the possible requirements and design considerations for such an application:

The script below is the first that gets invoked as part of launching the application:

    <?php
    // empdir_first.php

We include a set of utility functions here:

    require("empdir_functions.php");

This script is called again as a result of the user deciding to either add a new entry or to search for an existing entry:

    if (!isset($choice)) {
        generateHTMLHeader("Click below to access the Directory");
        generateFrontPage();
    } else if (strstr($choice, "ADD")) {
        $firstCallToAdd = 1;

For additions to the directory the empdir_add.php script is called:

        require("empdir_add.php");
    } else {
        $firstCallToSearch = 1;

For searching the directory, we call the empdir_search.php script:

        require("empdir_search.php");
    }
    ?>

This is how the initial screen would look to the user:

Click To expand

The script empdir_common.php contains some site-specific information that we need to customize to suit our environment:

    <?php
    //empdir_common.php

This conditional statement would ensure that this file does not get included multiple times:

    //Avoid multiple include()
    if (isset($EMPDIR_CMN)) {
        return;
    } else {
        $EMPDIR_CMN = true;
    }
    //Customize these to your environment

This is the base DN of our company directory:

    $baseDN = "o=Foo Widgets, c=us";

Below is the fully qualified hostname and port number of the LDAP server. We use OpenLDAP in this case – however, the code should work fine with any standard LDAP server:

    $ldapServer = "www.foowid.com";
    $ldapServerPort = 4321;
    ?>

As mentioned earlier, empdir_functions.php has a common set of functions used by other scripts. The functions are of two types – display related functions that print the HTML and utility functions such as those that encapsulate the logic of connecting and binding to the directory:

    <?php
    // empdir_functions.php
    // Common functions go here
    // Avoid multiple includes of this file
    if (isset($EMPDIR_FUNCS)) {
        return;
    } else {
        $EMPDIR_FUNCS = "true";
    }

This function generates a standard HTML page with a heading passed to it as an argument. This ensures a uniform look for the pages of the application:

    function generateHTMLHeader($message)
    {
        printf ("<head> <title> Foo Widgets - Employee Directory </title>
                 </head>");
        printf("<body text=\"#000000\" bgcolor=\"#999999\" link=\"#0000EE\"
                      vlink=\"#551A8B\" alink=\"#FF0000\">\n");
        printf("<h1>Foo Widgets Employee Directory</h1><br><br>");
        printf("<table cellpadding=\"4\" cellspacing=\"0\"
                       border=\"0\" width=\"600\">");
        printf("<tr bgcolor=\"#dcdcdc\"><td><font face=\"Arial\"><b>");
        printf("%s</b></font><br></td>", $message);
        printf("<td align=\"right\">");
        printf("</font></td></tr>");
        printf("</table>");
        printf("<br>");
        printf("<br>");
    }

This function generates the first page seen in the earlier screenshot. It outputs an HTML form which allows the user to choose between searching for entries or adding a new entry:

    function generateFrontPage()
    {
        printf("<form method=\"post\" action=\"empdir_first.php\">");
        printf("<input type=\"submit\" name=\"choice\" value=\"SEARCH\">");
        printf("&nbsp; &nbsp; &nbsp;");
        printf("<input type=\"submit\" name=\"choice\" value=\"ADD\">");
        printf("<br>");
        printf("<br>");
        printf("<ul>");
         printf("<li> Search for employees by clicking <i>SEARCH FOR
                EMPLOYEE</i> </li>");
         printf("<li> Add new employees (Admin only) by clicking <i>ADD A NEW
                EMPLOYEE</i> </li>");
         printf("<li> Modify employee details by clicking <i>SEARCH FOR
                EMPLOYEES</i> first and then choosing the entry to
                Modify</li>");
         printf("<li> Delete an existing entry (Admin only) by clicking
                <i>SEARCH FOR EMPLOYEES</i> first and then choosing the entry to
                Delete</li>");
         printf("</form>");
    }

This function generates HTML that prompts the user for the administrator's password while attempting to delete a user entry from the directory. The hidden form fields are required to re-construct the DN of the entry that is to be deleted, provided the authentication succeeds. Such a scheme is more illustrative than the definitive method to do this since the focus is on LDAP APIs. In a production environment, this information should be stored in HTTP sessions:

    function promptPassword($mail, $ou, $actionScript)
    {
        printf("<form method=\"GET\" action=\"%s\">", $actionScript);
        printf("Admin Password: <input type=\"password\"
                name=\"adminpassword\">&nbsp;");
        printf("<input type=\"hidden\" name=\"mail\" value=\"%s\">",
                urlencode($mail));
        printf("<input type=\"hidden\" name=\"ou\" value=\"%s\">",
                urlencode($ou));
        printf("<input type=\"submit\" name=\"submit\" value=\"Submit\">");
        printf("</form>");
    }

Standard mechanism to print out an error message in HTML:

    function displayErrMsg($message)
    {
        printf("<blockquote><blockquote><blockquote><h3><font
                color=\"#cc0000\">%s</font></h3></blockquote>
                </blockquote></blockquote>\n", $message);
    }

This function encapsulates the connection to the LDAP server and also the binding to the appropriate part of the DN tree:

    function connectBindServer($bindRDN = 0, $bindPassword = 0)
    {
        global $ldapServer;
        global $ldapServerPort;
        $linkIdentifier = ldap_connect($ldapServer, $ldapServerPort);

        if ($linkIdentifier) {

If no RDN and password is specified, we attempt an anonymous bind, else we bind using the provided credentials:

            if (!$bindRDN && !$bindPassword) {
                if (!@ldap_bind($linkIdentifier)) {
                    displayErrMsg("Unable to bind to LDAP server !!");
                    return 0;
                }
            } else {
                if (!ldap_bind($linkIdentifier, $bindRDN, $bindPassword)) {
                    displayErrMsg("Unable to bind to LDAP server !!");
                    return 0;
                }
            }
        } else {
            displayErrMsg("Unable to connect to the LDAP server!!");
            return 0;
        }
        return $linkIdentifier;
    }

Given a search criteria string, this function creates a search filter expression:

    function createSearchFilter($searchCriteria)
    {
        $noOfFieldsSet = 0;
        if ($searchCriteria["cn"]) {
            $searchFilter = "(cn=*" . $searchCriteria["cn"] . "*)";
            ++$noOfFieldsSet;
        }

        if ($searchCriteria["sn"]) {
            $searchFilter .= "(sn=*" . $searchCriteria["sn"] . "*)";
            ++$noOfFieldsSet;
        }

        if ($searchCriteria["mail"]) {
            $searchFilter .= "(mail=*" . $searchCriteria["mail"] . "*)";
            ++$noOfFieldsSet;
        }

        if ($searchCriteria["employeenumber"]) {
            $searchFilter .= "(employeenumber=*" .
                              $searchCriteria["employeenumber"] . "*)";
            ++$noOfFieldsSet;
        }

        if ($searchCriteria["ou"]) {
            $searchFilter .= "(ou=*" . $searchCriteria["ou"] . "*)";
            ++$noOfFieldsSet;
        }

        if ($searchCriteria["telephonenumber"]) {
            $searchFilter .= "(telephonenumber=*" .
                $searchCriteria["telephonenumber"] . "*)";
            ++$noOfFieldsSet;
        }

We perform a logical AND on all specified search criteria to create the final search filter:

        if ($noOfFieldsSet >= 2) {
            $searchFilter = "(&" .$searchFilter. ")";
        }
        return $searchFilter;
    }

This function; given a link identifier obtained from the connectBindServer() function and the search filter created by createSearchFilter(), performs a search on the directory:

    function searchDirectory($linkIdentifier, $searchFilter)
    {
        global $baseDN;
        $searchResult = ldap_search($linkIdentifier, $baseDN, $searchFilter);

We count the search results to see if we got any entries at all:

        if (ldap_count_entries($linkIdentifier, $searchResult) <= 0) {
            displayErrMsg("No entries returned from the directory");
            return 0;
        } else {
            $resultEntries = ldap_get_entries($linkIdentifier, $searchResult);
            return $resultEntries;
        }
    }

This function prints the result of a search as an HTML table:

    function printResults($resultEntries)
    {
        printf("<table border width=\"100%%\" bgcolor=\"#dcdcdc\" nosave>\n");
        printf("<tr><td><b>First Name</b></td>
                <td><b>Last Name</b></td>
                <td><b>E-mail</b></td>
                <td><b>Employee #</b></td>
                <td><b>Department</b></td>
                <td><b>Telephone</b></td>
                <td><b>Edit</b></td>
                </tr></b>\n");

        $noOfEntries = $resultEntries["count"];

        for ($i = 0; $i < $noOfEntries; $i++) {
            if (!$resultEntries[$i]["cn"] && !$resultEntries[$i]["sn"])
                continue;
            $mailString = urlencode($resultEntries[$i]["mail"][0]);
            $ouString = urlencode($resultEntries[$i]["ou"][0]);
            printf("<tr><td>%s</td>
                    <td>%s</td>
                    <td>%s</td>
                    <td>%s</td>
                    <td>%s</td>
                    <td>%s</td>
                    <td>
                    <a href=\"empdir_modify.php?mail=%s&ou=%s&firstCall=1\">
                      [Modify]</a>
                    <a href=\"empdir_delete.php?mail=%s&ou=%s\">
                      [Delete]</a><td>
                    </tr>\n",
                    $resultEntries[$i]["cn"][0],
                    $resultEntries[$i]["sn"][0],
                    $resultEntries[$i]["mail"][0],
                    $resultEntries[$i]["employeenumber"][0],
                    $resultEntries[$i]["ou"][0],
                    $resultEntries[$i]["telephonenumber"][0],
                    $mailString, $ouString,
                    $mailString, $ouString);
        }
        printf("</table>\n");
    }

This function is used by the script that creates a new entry and the script that modifies an existing entry. The function prints out a set of text fields that the user can fill or modify. In the modification case, preexisting values are provided as default values:


    function generateHTMLForm($formValues, $actionScript, $submitLabel)
    {
        printf("<form method=\"post\" action=\"%s\"><pre>\n", $actionScript);
        printf("First Name:&nbsp;&nbsp;<input type=\"text\" size=\"35\"
                                        name=\"cn\" value=\"%s\"><br>\n",
                ($formValues) ? $formValues[0]["cn"][0] : "");
        printf("Last Name:&nbsp;&nbsp;&nbsp;<input type=\"text\" size=\"35\"
                                                   name=\"sn\"
                                                   value=\"%s\"><br>\n",
                ($formValues) ? $formValues[0]["sn"][0] : "");
        printf("E-mail:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<input type=\"text\"
                size=\"35\" name=\"mail\" value=\"%s\"><br>\n", ($formValues) ?
                $formValues[0]["mail"][0] : "");
        printf("Employee no.:<input type=\"text\" size=\"35\"
                                    name=\"employeenumber\"
                                    value=\"%s\"><br>\n",
                ($formValues) ? $formValues[0]["employeenumber"][0] : "");
        printf("Department:&nbsp;&nbsp;<input type=\"text\" size=\"35\"
                                              name=\"ou\" value=\"%s\"><br>\n",
                ($formValues) ? $formValues[0]["ou"][0] : "");
        printf("Telephone:&nbsp;&nbsp;&nbsp;<input type=\"text\" size=\"35\"
                name=\"telephonenumber\" value=\"%s\"><br>\n", ($formValues) ?
                $formValues[0]["telephonenumber"][0] : "");

If this function is called from the modification script, it outputs an extra text field for the password of the user modifying the entry corresponding to them:

        if ($submitLabel == "MODIFY") {
            printf("User Password:&nbsp;&nbsp;&nbsp;&nbsp;
                    <input type=\"password\" size=\"35\"
                           name=\"userpassword\"><br>\n");
        }

If the function is called from the script responsible for adding users, it outputs a text field to prompt the user for the administrator's password:

        if ($submitLabel == "ADD") {
            printf("Admin Password:&nbsp;&nbsp;&nbsp;&nbsp;
                   <input type=\"password\" size=\"35\"
                          name=\"adminpassword\"><br>\n");
        }
        printf("<input type=\"submit\" value=\"%s\">", $submitLabel);
        printf("</pre></form>");
    }

This function merely provides a link to the main page:

    function returnToMain()
    {
        printf("<br><form action=\"empdir_first.php\" method=\"post\">\n");
        printf("<input type=\"submit\" VALUE=\"Click\">
                to return to Main Page\n");
    }

The cleanup function which closes the connection specified by the link identifier argument:

    function closeConnection($linkIdentifier)
    {
        ldap_close($linkIdentifier);
    }
    ?>

This script is invoked when the user clicks the SEARCH button. The search screen would look like below:

Click To expand
    <?php
    // empdir_search.php
    include("empdir_common.php");

We set the search filter to be a null string initially:

    $searchFilter = "";

Since this script is also called to process the search operation apart from being called to display the screen for entering search criteria, we need to distiniguish between the two cases:


    if (isset($firstCallToSearch)) {
        generateHTMLHeader("Search using the following criteria:");
        generateHTMLForm(0, "empdir_search.php", "SEARCH");
    } else {
        require("empdir_functions.php");

If all of the fields are empty, we print an error and re-display the screen:

        if (!$cn && !$sn && !$mail && !$employeenumber && !$ou &&
            !$telephonenumber) {
            generateHTMLHeader("Search using the following criteria:");
            displayErrMsg("Atleast one of the fields must be filled !!");
            generateHTMLForm(0, "empdir_search.php", "SEARCH");
        } else {

We create an associative array with the search criteria that we shall later use as an argument to the search function:

            $searchCriteria["cn"]              = $cn;
            $searchCriteria["sn"]              = $sn;
            $searchCriteria["mail"]            = $mail;
            $searchCriteria["employeenumber"]  = $employeenumber;
            $searchCriteria["ou"]              = $ou;
            $searchCriteria["telephonenumber"] = $telephonenumber;
            $searchFilter = createSearchFilter($searchCriteria);

We connect to the server and do an anonymous bind:

            $linkIdentifier = connectBindServer();
            if ($linkIdentifier) {

This function call fetches the search results if the search succeeded. We display the results using the printResults() function:

                $resultEntries = searchDirectory($linkIdentifier,
                                                 $searchFilter);
                if ($resultEntries) {
                    generateHTMLHeader("Search Results:");
                    printResults($resultEntries);
                    returnToMain();
                } else {
                    returnToMain();
                }
            } else {
                displayErrMsg("Connection to LDAP server failed !!");
                closeConnection($linkIdentifier);
                exit;
            }
        }
    }
    ?>

This is a sample screen of search results:

Click To expand

This script is called when a user clicks on the Modify link in the Edit column of a search result:

    <?php
    // empdir_modify.php
    include("empdir_common.php");
    include("empdir_functions.php");

    if (isset($firstCall)) {

We re-create our search-filter. This time we need to perform a targeted search so that the search returns just the entry that the user intends to modify. Therefore we use the e-mail attribute as the search criteria:

        $searchFilter = "(mail=*" . urldecode($mail) . "*)";

We connect to the server and perform an anonymous bind:

        $linkIdentifier = connectBindServer();
        if ($linkIdentifier) {
            $resultEntry = searchDirectory($linkIdentifier, $searchFilter);
        } else {
            displayErrMsg("Connection to LDAP server failed !!");
        }

        generateHTMLHeader("Please modify fields: (e-mail & dept. cannot be
                            changed)");

We generate an HTML form of text fields populated with the current values of an entry. The users can edit these fields and click the MODIFY button:

        generateHTMLForm($resultEntry, "empdir_modify.php", "MODIFY");
        closeConnection($linkIdentifier);
    } else {

This block gets executed as a result of submitting the afore-mentioned form. The new parameters are gathered into an associative array to be passed to the server:

        $dnString = "mail=" . $mail . "," . "ou=". $ou . "," . $baseDN;
        $adminRDN = "cn=Admin," . $baseDN;
        $newEntry["cn"]              = $cn;
        $newEntry["sn"]              = $sn;
        $newEntry["employeenumber"]  = $employeenumber;
        $newEntry["telephonenumber"] = $telephonenumber;

We connect to the server and bind as the user who's DN is to be modified:

        $linkIdentifier = connectBindServer($dnString, $userpassword);
        if ($linkIdentifier) {
            if ((ldap_modify($linkIdentifier, $dnString, $newEntry)) == false) {
                displayErrMsg("LDAP directory modification failed !!");
                closeConnection($linkIdentifier);
                exit;
            } else {
                generateHTMLHeader("The entry was modified succesfully");
                returnToMain();
            }
        } else {
            displayErrMsg("Connection to LDAP server failed");
            exit;
        }
    }
    ?>

This is an example of a typical modification screen:

Click To expand

This function is invoked when the user clicks the Delete link in the Edit column of the search results:

    <?php
    //empdir_delete.php
    include("empdir_common.php");
    include("empdir_functions.php");

We create a string corresponding to the DN of the entry we intend to delete:

    $dnString = "mail=" . urldecode($mail) . ",ou=" . urldecode($ou) . "," .
                $baseDN;

The script prompts the user for the administrator's password since this is required for deleting entries from the directory:

    if (!isset($adminpassword)) {
        generateHTMLHeader("Administrator action:");
        promptPassword($mail, $ou, "empdir_delete.php");
        return;
    }

Here the DN of the administrator user is hard-coded. Ideally there can be a whole category of administrative users and the roles and privileges of these users can be managed by using the HTTP sessions in tandem with the LDAP implementation's authentication and authorization mechanism:

    $adminRDN = "cn=Admin," . $baseDN;

We connect to the server and bind as the administrator user:

    $linkIdentifier = connectBindServer($adminRDN, $adminpassword);
    if ($linkIdentifier) {

The actual deletion is performed using the DN string we constructed earlier:

        if (ldap_delete($linkIdentifier, $dnString) == true) {
            generateHTMLHeader("The entry was deleted succesfully");
            returnToMain();
        } else {
            displayErrMsg("Deletion of entry failed !!");
            closeConnection($linkIdentifier);
            exit;
        }
    } else {
        displayErrMsg("Connection to LDAP server failed!!");
        exit;
    }
    ?>

This script is invoked when the user clicks on the ADD button from the main screen:

    <?php
    //empdir_add.php
    if (isset($firstCallToAdd)) {
        generateHTMLHeader("Please fill in fields: (Name, Dept. and E-mail
                            mandatory)");
        generateHTMLForm(0, "empdir_add.php", "ADD");
    } else {
        require("empdir_common.php");
        require("empdir_functions.php");

At least, the name, e-mail, and department information should be entered. If this is not entered, we display an error and re-display the earlier form:

        if (!$cn || !$mail || !$ou) {
            generateHTMLHeader("Please fill in fields: ");
            displayErrMsg("Minimally Name, Dept. and E-mail fields are
                          required!!");
            generateHTMLForm(0, "empdir_add.php", "ADD");
        } else {

We collect the attributes of the new entry to be added in an associative array:

            $entryToAdd["cn"] = $cn;
            $entryToAdd["sn"] = $sn;
            $entryToAdd["mail"] = $mail;
            $entryToAdd["employeenumber"] = $employeenumber;
            $entryToAdd["ou"] = $ou;
            $entryToAdd["telephonenumber"] = $telephonenumber;
            $entryToAdd["objectclass"] = "person";
            $entryToAdd["objectclass"] = "organizationalPerson";
            $entryToAdd["objectclass"] = "inetOrgPerson";

Here we construct the DN corresponding to the new entry:

            $dnString = "mail=" . $mail . "," . "ou=". $ou . "," . $baseDN;

This is the root DN we shall bind to, before performing the add operation:

            $adminRDN = "cn=Admin," . $baseDN;

We connect to the server and bind as an administrator:

            $linkIdentifier = connectBindServer($adminRDN, $adminpassword);
            if ($linkIdentifier) {

The actual addition is done here:

                if (ldap_add($linkIdentifier, $dnString, $entryToAdd) == true) {
                    generateHTMLHeader("The entry was added succesfully");
                    returnToMain();
                } else {
                    displayErrMsg("Addition to directory failed !!");
                    closeConnection($linkIdentifier);
                    returnToMain();
                    exit;
                }
            } else {
                displayErrMsg("Connection to LDAP server failed!");
                exit;
            }
        }
    }
    ?>

A typical screen prompting the user to enter the attributes would look like the one below:

Click To expand

We need to be aware of certain caveats with this application that arise from the fact that this is merely illustrative of the PHP LDAP API and not a fully-fledged production application. As mentioned before the use of HTTP sessions is highly recommended to indicate authentication status. Further users created using the add mechanism do not have a password field and so modification of such entries is not possible through the current mechanism.

To get started with the application we could upload a sample set of user information into the directory using the ldapadd utility that comes with most LDAP client software and then work with it. A typical sample would look like:


    dn: o=Foo Widgets, c=us
    objectclass: top
    objectclass: organization
    o: Foo Widgets

    dn: ou=Engineering, o=Foo Widgets, c=us
    objectclass: top
    objectclass: organizationalUnit
    ou: Engineering

    dn: ou=Marketing, o=Foo Widgets, c=us
    objectclass: top
    objectclass: organizationalUnit
    ou: Marketing

    dn: mail=faginm@foowi.com, ou=Engineering, o=Foo Widgets, c=us
    cn: Fagin
    sn: Maddog
    objectclass: top
    objectclass: person
    objectclass: organizationalPerson
    objectclass: inetOrgPerson
    mail: faginm@foowi.com
    ou: Engineering
    employeenumber: 3123283622
    telephonenumber: 666-767-2000
    userpassword: faginm123

    dn: mail=maryx@foowi.com, ou=Marketing, o=Foo Widgets, c=us
    cn: Mary
    sn: Xeyed
    objectclass: top
    objectclass: person
    objectclass: organizationalPerson
    objectclass: inetOrgPerson
    mail: maryx@foowi.com
    ou: Marketing
    employeenumber: 3223453622
    telephonenumber: 111-767-2000
    userpassword: maryx123

Also, if we use OpenLDAP for running the application, so as to effect access control, we need to add the following lines to slapd.conf and restart slapd:

    access to attr=userPassword
            by self write
            by anonymous auth
            by * none

    access to *
            by self write
            by dn="cn=Admin,o=Foo Widgets,c=us" write
            by * read

The first block indicates that any user can modify their own password and can bind anonymously to the server to authenticate against the password stored in the respository. The second block indicates that a given user can modify their attributes and so can the admin user. It also indicates that all users have read only access to all other attributes of all other entities – thereby allowing any user to search the directory. For more information on access control in OpenLDAP, see the OpenLDAP administrator's guide: http://www.openldap.org/doc/admin/.


Table of Contents
Previous Next