LDAP basics

Client-Server

LDAP uses the familiar client-server architecture:
        +--------+              +--------+
        |  LDAP  |------------->|  LDAP  |
        | Client |              | Server |
        +--------+              +--------+
The client opens a connection using TCP, by default to port 389 [1]. It may then log in - this is known as "binding" [2]. If it has no credentials with which to authenticate, it can bind as "anonymous".

In this respect, you can consider that LDAP is very like FTP. In fact, the similarities don't end there:

Unlike FTP and many other Internet protocols, LDAP is too complicated to send queries by hand using 'telnet'. For example, simply binding as anonymous requires the following binary data to be sent:

        30 0c 02 01 01 60 07 02 01 02 04 00 80 00
I don't have a clue how you get that stream of bytes from the LDAP specification, which refers you to ITU-T X.690 for details. I found it out using strace:
        # strace -s100 -xx ldapsearch -b "dc=wibble,dc=org" "uid=root" 2>dump
and you can confirm it works by sending the same data [4].

Suffice to say, just don't bother trying to drive ldap manually; use someone else's tools instead.

Distinguished Names

All the data in the LDAP database is referenced by means of a "distinguished name" (DN). Because LDAP is designed to be scalable, the namespace is organised as a hierarchy. (Consider the difference between the DNS, which is a hierarchical system, and /etc/hosts, which is a flat file)
                     dc=wibble,dc=org
                             |
                             |
        +-----------------+------------------+
        |                 |                  |
cn=Fred Bloggs,     cn=Joe Smith,         ou=printers,
dc=wibble,dc=org    dc=wibble,dc=org      dc=wibble,dc=org
                                             |
                                             |
                               +---------------------------+
                               |                           |
                   cn=laserjet,ou=printers,        cn=epson,ou=printers,
                   dc=wibble,dc=org                dc=wibble,dc=org
Distinguished names are read from right to left, starting at the top of the hierarchy and going downwards. This is the same as the DNS, where for example in "andrew.cmu.edu" the top-level domain is "edu", the next level down is "cmu" and the next level is "andrew".

LDAP itself doesn't force a particular naming hierarchy; it's all data-driven and you can design your own if you really want. However there are two in common use:

  1. Distinguished Names based on DNS

    Existing DNS domain names can be mapped directly into the LDAP namespace [5], which saves you having to invent new names. As long as you have a "real" DNS domain name, which is globally unique, it ensures that your LDAP distinguished names are globally unique as well.

    You use "dc=" (domain component) for each part of the domain name. For example, if your domain is "wibble.org" it would have a DN of

            dc=wibble, dc=org
    
    The person called Fred Bloggs in your organisation could then be
            cn=Fred Bloggs, dc=wibble, dc=org
    
    cn stands for "Common Name". Note that it is quite OK for these identifiers to include spaces. Actually, LDAP is 8-bit clean and will happily store binary data such as JPEG photos [6].

    To identify a login account at that domain, you can use "uid=" (user id). So, the user "root@wibble.org" would have a DN of

            uid=root, dc=wibble, dc=org
    
  2. X500-style Distinguished Names

    X500, from which LDAP draws on heavily, has its own hierarchy [7]:

            cn=Fred Blogs, ou=Accounts, o=Wibble Widgets Ltd, c=GB
    
    ou=Organisational Unit, o=Organisation, c=Country. It can get much more complicated than this (e.g. you can add State/Province and Locality into the DN). The advantage of this approach is that you can present information to the world as a sort of LDAP "telephone book".

    For me, the main disadvantage is that I don't know where you go to get an Organisation name allocated which is guaranteed to be unique within a particular Country. For this reason, using a DN which is derived from your domain name is probably a safer bet.

The lefthand-most part of a DN by itself is called a Relative Distinguished Name (RDN):
   cn=Fred Bloggs,dc=wibble,dc=org       Distinguished Name (DN)
   cn=Fred Bloggs                        Relative Distinguished Name (RDN)
Note that there are a few rules when setting up a hierarchy of DNs:
  1. There must be an immediate parent entry for every entry in the tree, apart from the root. So for example, if you try to add an entry with DN "cn=epson,ou=printers,dc=wibble,dc=org", there must already be an entry with DN "ou=printers,dc=wibble,dc=org", otherwise the operation will fail.
  2. For all entries which share the same parent, their Relative Distinguished Names must be different. This ensures that every entry has a unique Distinguished Name.
  3. You can include spaces between the parts of a DN - so "dc=wibble,dc=org" is the same as "dc=wibble,  dc=org"
  4. If certain special characters appear within a DN, they must be escaped.

    Space at beginning or end of dn\space
    # at beginning of dn\#
    ,\,
    +\+
    "\"
    \\\
    <\<
    >\>
    ;\;

So for the organisation "Incredible Widgets, Ltd.", their DN might be
   o=Incredible Widgets\, Ltd.,c=GB

Entries and classes

The LDAP database is keyed by the Distinguished Name (DN), and each DN points to a single entry (record). Each entry belongs to a particular class, which defines what attributes (fields) it may contain. Some attributes may be mandatory, and others optional.

For example, suppose you are using LDAP to emulate the /etc/hosts file. When you lookup a host using an LDAP query, this is what you might get back:

                                          objectclass: ipHost
                                          +-------------------------+
  cn=localhost,dc=wibble,dc=org --------> | iphostnumber: 127.0.0.1 |
                                          +-------------------------+
Here, the DN we are using points to an entry of class "ipHost". This class contains one field, namely, "iphostnumber" which is the IP address we want. iphostnumber is a required field in an "ipHost" object, because it wouldn't make sense to leave it out.

Here is another example:

                                          objectclass: person
                                          +-------------------------------+
  cn=Fred Bloggs,dc=wibble,dc=org ------> | cn: Fred Bloggs               |
                                          | sn: Bloggs                    |
                                          | telephoneNumber: 800 123 4567 |
                                          | description: blue eyes        |
                                          +-------------------------------+
This object class, "person", contains fields which are relevant to a human being. In this particular class, "cn" and "sn" are required fields, whilst telephoneNumber and description are optional.

Class hierarchies

You can define your own object classes, which means that LDAP is totally flexible to be adapted to your needs. However, rather than inventing a new class from scratch, it is useful to be able to extend an existing class. This means that the classes themselves also form a hierarchy.

For example, the "person" class defined above has very few fields in it. So there is also another class called "organizationalPerson" which has more detail in it, specific to a person working at a company.

                                          objectclass: organizationalPerson
                                          +-------------------------------+
  cn=Fred Bloggs,dc=wibble,dc=org ------> | cn: Fred Bloggs               |
                                          | sn: Bloggs                    |
                                          | telephoneNumber: 800 123 4567 |
                                          | description: blue eyes        |
                                          | title: Head of Accounts       |
                                          | postalAddress: ....           |
                                          +-------------------------------+
This class "organizationalPerson" is a subclass of "person". The advantage of building a class hierarchy like this is that a client which only understands a "person" record can still process data from an "organizationalPerson" record.

All classes ultimately derive from the top-level class "top". So the class hierarchy of what we have seen so far is:

                        top
                         |
             +-----------+----------+
             |                      |
           person                 ipHost
             |
             |
    organizationalPerson
The objectclass hierarchy is completely separate from the distinguished name hierarchy.

Using the OpenLDAP server, you can find definitions of the fields required and allowed for each objectclass in /etc/openldap/schema/

The standard classes like "person" and "organizationalPerson" are defined in RFC 2256; an extended class called inetOrgPerson is defined in RFC 2798.


[1] RFC 2251 (LDAP v3) section 5.2.1

[2] RFC 2251 (LDAP v3) section 4.2

[3] RFC 2222 (SASL)

[4] Simple perl program to send that LDAP bind in binary:

#!/usr/bin/perl -w
use strict;
use Socket;

my($remote,$port,$iaddr,$paddr,$proto,$line);

$remote = 'localhost';
$port = 389;

$iaddr = inet_aton($remote) or die "no host: $remote";
$paddr = sockaddr_in($port, $iaddr);

$proto = getprotobyname('tcp');
socket(SOCK, PF_INET, SOCK_STREAM, $proto) or die "socket: $!";
connect(SOCK, $paddr) or die "connect: $!";

$line = pack("H28", "300c020101600702010204008000");
syswrite (SOCK, $line, 14);
sysread (SOCK, $line, 14);
print STDERR "Response: ", unpack("H28", $line), "\n";

close (SOCK);
exit;
Of course, you'd never do it that way in real life. Rather, you would use an API (Application Programming Interface) to do the encoding and decoding of LDAP strings. For example, in perl you'd use perldap, available from www.mozilla.org or as a port in FreeBSD ('ldapsdk' and 'perldap'). So this becomes:
#!/usr/bin/perl
use Mozilla::LDAP::Conn;

$who="";
$passwd="";

$ld = new Mozilla::LDAP::Conn("ldap.itd.umich.edu", "389", $who, $passwd);
die "Bind failed\n" unless $ld;
print "Bind succeeded\n";
$ld->close;
Warning: I found that ldapsdk and openldap have conflicting "libldap.so" libraries, which meant that the above example did not work if openldap was installed at the same time. I'm not sure about the right way to fix this, it probably involves fiddling with the library path.

[5] RFC 2247 (Using Domains in LDAP/X.500 Distinguished Names) and RFC 2377 (Naming Plan for Internet Directory-Enabled Applications)

[6] The OpenLDAP server uses base64 encoding when exporting binary data in textual form. The second of these two examples includes a newline in the data:

        # echo -n "testing" | ldif cn
        cn: testing
        # echo "testing" | ldif cn
        cn:: dGVzdGluZwo=

[7] RFC 2256 (A Summary of the X.500(96) User Schema for use with LDAPv3)