The LDAP Models

   

LDAP defines four basic models that fully describe how it operates, what data can be stored in LDAP directories, and what can be done with that data.

The LDAP Information Model

The LDAP information model defines the types of data and basic units of information you can store in your directory. In other words, the LDAP information model describes the building blocks you can use to create your directory.

Entries, Attributes, and Values

The basic unit of information in the directory is the entry , a collection of information about an object. Often the information in an entry describes a real-world object such as a person, but the model does not require this. In a typical directory you'll find thousands of entries that correspond to people, departments, servers, printers, and other real-world objects in the organization served by the directory. Figure 2.7 shows a portion of a typical directory, with objects corresponding to some of the real-world objects in the organization.

Figure 2.7. Part of a Typical Directory

Each directory entry has a distinguished name (DN); for example, the organization shown in Figure 2.7 has the DN dc=example,dc=com . We will discuss DNs in detail later in this chapter when we dive into the LDAP naming model. An entry is composed of a set of attributes , each of which describes one particular trait of the object. Each attribute has a type and one or more values . The type describes the kind of information contained in the attribute, and the value contains the actual data. For example, Figure 2.8 zooms in on an entry describing a person, with attributes for the person's full name, surname (last name), telephone number, and e-mail address.

Figure 2.8. A Directory Entry Showing Attribute Types and Values

Note

Throughout this book you'll see directory entries shown in the LDIF (LDAP Data Interchange Fromat) text format. This is a standard way of representing directory data in a textual format, and it is used when data is being exported from and imported into a directory server. We'll describe LDIF in detail later in this chapter.


Attribute types also have an associated syntax and a set of matching rules . The syntax of an attribute specifies the form of the data that may be present in an attribute of that type. For example, the INTEGER syntax allows only digits to be present in a value. Matching rules specify the following things:

  • The rules used for comparing values for equivalence . For example, the caseIgnoreMatch rule specifies that case is not significant when values are being searched or replaced . In comparisons of the values Smith and smith , they are considered equivalent if the caseIgnoreMatch matching rule is used.

  • The rules used for sorting values . For example, the caseIgnoreMatch rule specifies that values are ordered lexicographically, but the integerMatch rule specifies that values are ordered according to their numerical values.

All standards-compliant LDAP server software supports a set of well-known required syntaxes and matching rules; see RFC 2252 (http://www.ietf.org/rfc/rfc2252.txt) for a list. Some packages, such as Netscape Directory Server, provide a plug-in interface or another mechanism that allows additional syntaxes or matching rules to be added.

Attributes are also classified broadly into two categories: user and operational. User attributes , the "normal" attributes of an entry, may be modified by the users of the directory (with appropriate permissions). Operational attributes are special attributes that either modify the operation of the directory server or reflect the operational status of the directory. An example of an operational attribute is the modifyTimeStamp attribute, which reflects the time that the entry was last modified and is automatically maintained by the directory. When an entry is sent to a client, operational attributes are not included unless the client requests them by name.

Attribute values can also have additional constraints placed on them. Some server software allows the administrator to declare whether a given attribute type may hold multiple values or whether only a single attribute value may be stored. For example, the givenName attribute is typically multivalued so that a person can include more than one given name (for example, Jim and James ). On the other hand, an attribute holding an employee ID number is likely to be single-valued. Some server software allows the administrator to set the maximum length of an attribute's values. This feature can be used to prevent directory users from using unreasonable amounts of storage.

Maintaining Order: Directory Schemas

Any entry in the directory has a set of required attribute types and a set of allowed attribute types. For example, an entry describing a person is required to have a cn (common name) attribute and an sn (surname) attribute. Other attributes are allowed, but not required, for person entries. Any attribute type not explicitly required or allowed is prohibited . The collections of all information about required and allowed attributes are called the directory schemas .

It's important to understand that directory schemas do not have any bearing on the arrangement of entries into the LDAP directory tree. This may seem odd if you are familiar with relational database technology, where schema is a more inclusive term describing the layout of database tables and their relationship to one another. The LDAP schema is a simpler concept, and therefore it determines only what types of data appear in an individual entry.

Directory schemas, which are discussed in detail in Chapter 8, Schema Design, allow you to retain control and maintain order over the types of information stored in your directory.

In summary, the LDAP information model describes entries , which are the basic building blocks of your directory. Entries are composed of attributes, which are composed of an attribute type and one or more values. Attributes may have constraints that limit the type and length of data placed in attribute values. The directory schemas place restrictions on the attribute types that must be or are allowed to be contained in an entry.

The LDAP Naming Model

The LDAP naming model defines how you organize and refer to your data. In other words, it describes the types of structures you can build out of your individual building blocks, which are the directory entries. After you've arranged your entries into a logical structure, the naming model also tells you how to refer to any particular directory entry within that structure.

The flexibility afforded by the LDAP naming model allows you to place your data in the directory in a way that is easy for you to manage. For example, you might choose to create one container to hold all the entries describing people in your organization, and another container to hold all your groups. Alternatively, you might choose to arrange your directory in a way that reflects the geographical placement of your organization's offices. Chapter 9, Namespace Design, guides you in making good choices when you design your directory hierarchy or namespace.

The LDAP naming model specifies that entries should be arranged in an inverted tree structure, as shown in Figure 2.9. Readers familiar with the hierarchical file system used by Unix systems will note its similarities to this directory structure. Such a file system consists of a set of directories and files; each directory may have zero or more files or directories beneath it. Figure 2.10 shows part of a typical Unix file system.

Figure 2.9. A Directory Tree

Figure 2.10. Part of a Typical Unix File System

There are three significant differences between the Unix file system hierarchy and the LDAP directory hierarchy, however. The first major difference between the two models is that there isn't really a root entry in the LDAP model. A file system, of course, has a root directory, which is the common ancestor of all files or directories in the file system hierarchy. In an LDAP directory hierarchy, on the other hand, the root entry is a special entry that contains configuration information about the directory server. It is not normally used to store information.

The second major difference is that in an LDAP directory, every node contains data, and any node can be a container. This means that any LDAP entry may have child nodes underneath it. In contrast, in a file system a given node is either a file or a directory, but not both. In the file system, only directories may have children, and only files may contain data. Another way of thinking of this is that an entry in a directory may be both a file and a directory simultaneously . The directory tree shown in Figure 2.11 illustrates this concept. Notice how the entries dc=example , dc=com , ou=People , and ou=Devices all contain data (attributes) but are also containers with child nodes beneath them.

Figure 2.11. Part of a Typical LDAP Directory

The third and final difference between the file system hierarchy and the LDAP hierarchy is how individual nodes in the tree are named. LDAP names are backward relative to file system names. As illustration, consider the names of the shaded nodes in Figures 2.10 and 2.11. In Figure 2.10, the shaded node is a file with a complete filename of /usr/bin/grep . Notice that if you read the filename from left to right, you move from the top of the tree ( / ) down to the specific file being named.

Contrast this name with the name of the shaded directory entry in Figure 2.11: uid=bjensen,ou=people,dc=example,dc=com . Notice that, if you read from left to right, you move from the specific entry being named back up toward the top of the tree. We'll discuss directory entry names in detail in Chapter 9, Namespace Design.

Although LDAP supports a hierarchical arrangement of directory entries, it does not mandate any particular type of hierarchy. Just as you're free to arrange your file system in a way that makes sense to you and is easy for you to manage, you're free to construct any type of directory hierarchy you want. Of course, some directory structures are better than others, depending on your particular situation; we'll cover the topic of designing your directory namespace in Chapter 9.

The one exception to this freedom is if your LDAP directory service is actually a front end to an X.500 service. The X.500 naming model is much more restrictive than the LDAP naming model. In the X.500 1993 standard, directory structure rules limit the types of hierarchies you can create. The standard accomplishes this restriction by specifying what types of "object classes" may be direct children of an entry. In the X.500 model, for example, only entries representing countries , localities, or organizations may be placed at the root of the directory tree. The LDAP naming model, on the other hand, does not limit the tree structure in any way; any type of entry may be placed anywhere in the tree.

In addition to specifying how to arrange directory entries into hierarchical structures, the LDAP naming model describes how to refer to individual entries in the directory. We mentioned this briefly when we were discussing the similarities and differences between file system hierarchy and LDAP directory hierarchy. Now let's go into more detail about naming.

Why Is Naming Important?

A naming model is needed so that you can give a unique name to any entry in the directory, allowing you to refer to any entry unambiguously. In LDAP, distinguished names (DNs) are how you refer to entries.

Like file system pathnames, we form the name of an LDAP entry by connecting in a series all the individual names of the parent entries back to the root. For example, look back at the directory tree shown in Figure 2.11. The shaded entry's name is uid=bjensen,ou=people,dc=example,dc=com . Reading this name from left to right, you can trace the path from the entry itself back to the root of the directory tree. The individual components of the name are separated by commas. Spaces after the commas are optional, so the following two distinguished names are equivalent:

 uid=bjensen, ou=people, dc=example, dc=com uid=bjensen,ou=people,dc=example,dc=com 

In any entry's DN, the leftmost component is called the relative distinguished name ( RDN ). Among a set of peer entries (those that share a common immediate parent), each RDN must be unique. This rule, when applied recursively to the entire directory tree, ensures that no two entries have the same DN. If you attempt to add two entries with the same name, the directory server will reject the attempt to add the second entry; this is similar to a Unix or Microsoft Windows file system, which will reject an attempt to create a file that has the same name as an existing file within a directory.

RDNs have to be unique only if they share a common immediate parent. Look at the tree in Figure 2.12. Even though two entries have the RDN cn=John Smith , they are in different subtrees, so the tree is completely legal. Whether this is a good way to construct your directory is another matter, one addressed in Chapter 9, Namespace Design.

Figure 2.12. Entries with the Same RDNs Are Permitted If They Are in Different Parts of the Tree

Multivalued RDNs, and Why You Should Avoid Using Them

You've probably noticed that each RDN we've shown is composed of two parts: an attribute name and a value, separated by an equal sign ( = ). An RDN may also contain more than one such name “value pair. Such a construction, called a multivalued RDN , looks like this:

 cn=John Smith + mail=jsmith@example.com 

The RDN for this entry consists of two attribute=value pairs: cn=John Smith and mail=jsmith@example.com .

Multivalued RDNs can be used to distinguish RDNs that would otherwise be the same. For example, if there were more than one John Smith entry in the same container, a multivalued RDN would allow you to assign unique RDNs to each entry. However, you should generally avoid using multivalued RDNs, for two reasons: First, they tend to clutter your namespace, and there are better ways to arrive at unique names for your entries. (Approaches for uniquely naming your entries are discussed in Chapter 9, Namespace Design.) Second, according to X.500 specifications, from which LDAP is derived, the values in a multivalued RDN are a set, which means that the ordering is not significant. Therefore, the following two DNs refer to the same entry:

 cn=John Smith + mail=jsmith@example.com, dc=example, dc=com mail=jsmith@example.com + cn=John Smith, dc=example, dc=com 

Client applications that need to compare DNs must be sophisticated enough to understand that these two DNs are equivalent. Realistically, few applications handle this correctly. Also, many off-the-shelf directory applications do not allow you to create entries with multivalued RDNs, nor do they properly handle any such entries that may exist in your directory. Our advice is to avoid using multivalued RDNs in your directory.

Escaping

Certain characters must be escaped when they appear within a component of a DN. For example, what if you have the entry o=United Widgets,Ltd. in your directory? The comma is part of the organization's name, not a separator between DN components. To resolve the ambiguity, you must escape all literal commas (those within an RDN) with a backslash ( \ ). In our example, then, the DN would be

Table 2.1. Characters That Must Be Escaped If Contained in Distinguished Names

Character

Decimal Value

Escape Sequence

Space at the beginning or end of a DN or RDN

32

\<space>

Octothorp ( # ) character at the beginning of a DN or RDN

35

  \#  

Comma ( , )

44

  \,  

Plus sign ( + )

43

  \+  

Double quote ( " )

34

  \"  

Backslash ( \ )

92

  \  

Less-than symbol ( < )

60

  \<  

Greater-than symbol ( > )

62

  \>  

Semicolon ( ; )

59

  \;  
 o=United Widgets\, Ltd., c=GB 

Table 2.1 shows all the characters that must be escaped, according to the LDAPv3 specification.

Aliases

Alias entries in the LDAP directory allow one entry to point to another one, which means that you can devise structures that are not strictly hierarchical. Alias entries perform a function like symbolic links in the Unix file system or shortcuts in the Windows 95/NT file system. In Figure 2.13, the dotted entry is an alias entry pointing to the real entry.

Figure 2.13. An Alias Entry Points to Another Directory Entry

To create an alias entry in the directory, you must first create an entry with the object class alias and an attribute named aliasedObjectName . The value of the aliasedObjectName attribute must be the DN of the entry you want this alias to point to.

In general, we recommend that you avoid the use of aliases. Because aliases can point to any directory entry, even one that is on a different server, aliases may exact a severe performance penalty. Consider the directory trees shown in Figure 2.13. Alias entries in one of the trees point to entries in the other tree, which is housed in another server. To support searching across the entire dc=example,dc=com tree, Server A must contact Server B each time an alias entry is encountered while the search operation is being serviced.

This requirement can significantly slow down searches. In addition, when an entry is deleted on one server, it might still be referred to by an alias on another server. The result can be a dangling reference, which must be cleaned up somehow.

Instead of using aliases, use referrals or LDAP URLs in entries to point to the information you need to reference. More information on using referrals can be found in Chapter 10, Topology Design.

The LDAP Functional Model

Now that you understand the LDAP information and naming models, you need some way to access the data stored in the directory tree. The LDAP functional model describes the operations that you can perform on the directory using the LDAP protocol.

The LDAP functional model consists of a set of operations divided into three groups. The interrogation operations allow you to search the directory and retrieve directory data. The update operations allow you to add, delete, rename, and change directory entries. The authentication and control operations allow clients to identify themselves to the directory and control certain aspects of a session.

In addition to these three main groups of operations, version 3 of the LDAP protocol defines a framework for adding new operations to the protocol via LDAP extended operations . Extended operations allow the protocol to be extended in an orderly fashion to meet new marketplace needs as they emerge. Extended operations were described earlier, in the section titled LDAP Extensibility.

The LDAP Interrogation Operations

The two LDAP interrogation operations allow LDAP clients to search the directory and retrieve directory data. The search operation allows a client to find entries in the directory, and the compare operation allows a client to test whether an entry contains a particular attribute value.

The LDAP Search Operation

The LDAP search operation is used to search the directory for entries and retrieve individual directory entries. There is no LDAP read operation. When you want to read a particular entry, you must use a form of the search operation in which you restrict your search to just the entry you want to retrieve. Later in the chapter we'll discuss how to search the directory and retrieve specific entries, as well as how to list all the entries at a particular location in the tree.

The LDAP search operation requires eight parameters:

  1. Base object for the search

  2. Search scope

  3. Alias dereferencing options

  4. Size limit

  5. Time limit

  6. Attributes-only parameter

  7. Search filter

  8. List of attributes to return

Base Object

The first parameter is the base entry for the search (the terms entry and object are used interchangeably). This parameter, expressed as a DN, indicates the top of the tree you want to search.

Search Scope

The second parameter is the scope. There are three types of scope. A scope of sub (subtree) indicates that you want to search the entire subtree from the base object all the way down to the leaves of the tree. A scope of onelevel indicates that you want to search only the immediate children of the entry at the top of the search base. A scope of base indicates that you want to limit your search to just the base object; this scope is used to retrieve one particular entry from the directory. Figure 2.14 depicts the three search scope types. The base object and the search scope together define the area of the directory tree you want to search.

Figure 2.14. The Three Types of Search Scope

Alias Dereferencing Options

The third search parameter, derefAliases , tells the server whether aliases should be dereferenced when it is performing the search. This parameter has four possible values:

  1. neverDerefAliases . Do not dereference aliases in searching or in locating the base object of the search.

  2. derefInSearching . Dereference aliases in searching subordinates of the base object, but not in locating the base object of the search.

  3. derefFindingBaseObject . Dereference aliases in locating the base object of the search, but not in searching subordinates of the base object.

  4. derefAlways . Dereference aliases both in searching subordinates of the base object and in locating the base object of the search.

Size Limit

The fourth search parameter is the size limit. This parameter tells the server that the client is interested in receiving only a certain number of entries. For example, if the client passes a size limit of 100, but the server locates 500 matching entries, only the first 100 will be returned to the client, along with a result code of LDAP_SIZELIMIT_ EXCEEDED . A size limit of 0 means that the client wants to receive all matching entries. (Note that servers may impose a maximum size limit that cannot be overridden by unprivileged clients.)

Time Limit

The fifth search parameter is the time limit. This parameter tells the server the maximum time in seconds that it should spend trying to honor a search request. If the time limit is exceeded, the server will stop processing the request and send a result code of LDAP_TIMELIMIT_EXCEEDED to the client. A time limit of 0 indicates that no limit should be in effect. (Note that servers may impose a maximum time limit that cannot be overridden by unprivileged clients.)

Attributes-Only Parameter

The sixth search parameter, attrsOnly , is a Boolean parameter. If it is set to true , the server will send only the attribute types to the client; attribute values will not be sent. This parameter can be used if the client is interested in finding out which attributes are contained in an entry but not in receiving the actual values. If this parameter is set to false , attribute types and values will be returned.

Search Filter

The seventh search parameter is the search filter, an expression that describes the types of entries to be returned. The filter expressions used in LDAP search operations are flexible; they are discussed in detail in the next section, The LDAP Search Filters.

Proposed Method for Retrieving All Operational Attributes

A proposal under review by the Internet Engineering Task Force (IETF) would allow the plus sign ( + ) to be used to request all operational attributes. If the proposal is adopted and implemented, clients will be able to request all operational attributes contained in an entry without knowing in advance the attribute names. For more information, see the Internet Draft LDAPv3: All Operational Attributes (http://www.ietf.org/internet-drafts/draft-zeilenga-ldapv3bis-opattrs-06.txt).

List of Attributes to Return

The eighth and final search parameter is a list of attributes to be returned for each matching entry. If this list is empty, all user attributes are returned. The special value * also means that all user attributes are to be returned, but it allows you to specify additional nonuser (operational) attributes that should be returned. (Without this special value, there would be no way to request all user attributes plus some operational attributes.)

Occasionally, you will want to verify that an entry exists but you won't be interested in retrieving any of the attributes. If you want to retrieve no attributes at all, you should specify the attribute name 1.1 . Table 2.2 provides some examples of attribute lists and the corresponding attributes returned by the server.

Note

Readers familiar with the ldapsearch command-line utility will note that it's not necessary to supply all the search parameters just discussed. The reason is that the ldapsearch utility provides default values for all options except the base object and the search filter.


Table 2.2. Examples of Attribute Lists and Corresponding Attributes Returned by the Server

Attribute List

Attributes Returned

cn, sn, givenName

cn , sn , and givenName only

*

All user attributes

1.1

No attributes

modifiersName

modifiersName only (an operational attribute)

*, modifiersName

All user attributes plus modifiersName

The LDAP Search Filters

An LDAP filter is a Boolean combination of attribute “value assertions. An attribute “value assertion consists of two parts: an attribute name and a value assertion, which you can think of as a value with wildcards allowed. The following sections look at the various types of search filters. We use the RFC 2254 notation for LDAP search filters, which is convenient because the filters are encoded as text strings (within the LDAP protocol itself, the filters are encoded with BER, as discussed earlier). This notation is also what you use when using the ldapsearch command-line utility, which will be discussed later in this chapter.

Equality Filters

An equality filter allows you to look for entries that exactly match a particular value. Here's an example:

 (sn=smith) 

This filter matches entries in which the sn (surname) attribute contains a value that is exactly smith . Because the equality matching rule associated with the sn attribute is caseIgnoreMatch , the case of the attribute and the filter is not important when matching entries are being located.

Substring Filters

When you use wildcards in filters, they are called substring filters . For example, the filter (sn=smith*) matches any entry that has an sn attribute value that begins with "smith". Entries with a surname of Smith , Smithers , Smithsonian , and so on will be returned.

Wildcards may appear anywhere in the filter expression, so the filter (sn=*smith) matches entries in which the surname ends with "smith" (for example, Blacksmith ). The filter (sn=smi*th) matches entries in which the surname begins with "smi" and ends with "th", and the filter (sn=*smith*) matches entries that contain the string "smith" in the surname attribute. Note that the wildcard character matches zero or more instances of any character, so the filter ( sn=*smith* ) would match the entry with the surname Smith as well as any surnames in which the string "smith" was embedded.

Approximate Filters

In addition to the equality and substring filters, servers support an approximate filter . For example, on most directory servers, the filter (sn~=jensen) returns entries in which the surname attribute has a value that sounds like "jensen" (for example, jenson ). Exactly how the server implements this filter is particular to each vendor and the languages supported by the server. Netscape Directory Server, for example, uses the metaphone algorithm to locate entries when an approximate filter is used. Internationalization also throws an interesting wrinkle into the concept of approximate matching; each language may need its own particular sounds ”like algorithms. For example, algorithms used to implement approximate matching for English are different from those for Japanese. It's likely that your directory software supports approximate matching for English but not for other languages.

"Greater Than or Equal To" and "Less Than or Equal To" Filters

LDAP servers also support "greater than or equal to" and "less than or equal to" filters on attributes that have some inherent ordering. For example, the filter (sn<=Smith) returns all entries in which the surname is less than or equal to Smith lexicographically. The ordering used depends on the matching rules associated with a particular attribute. The sn attribute, which is defined with the caseIgnoreOrderingMatch matching rule, is ordered lexicographically without respect to case. An attribute that has INTEGER syntax would be defined with a matching rule that would order values numerically . Attributes that have no inherent ordering, such as JPEG photos, cannot be searched for with this type of filter.

If you find that you need a greater-than or less-than filter (without the equals part), note that "greater than" is the complement of "less than or equal to" and "less than" is the complement of "greater than or equal to." In other words, (age>21) is equivalent to (!(age<=21)) . Similarly, the filter (age<21) , which is also not a valid LDAP filter, is equivalent to (!(age>=21)) .

In these cases, ! is the negation operator, which we will discuss in more detail shortly.

Presence Filters

Another type of search filter is the presence filter . It matches any entry that has at least one value for the attribute. For example, the filter (telephoneNumber=*) matches all entries that have a telephone number.

Extensible Matching

The last type of search filter is the extensible match filter . It is supported only by LDAPv3 servers. The purpose of an extensible match filter is to allow new matching rules to be implemented in servers and used by clients. Recall our earlier example involving the caseIgnoreMatch rule. Each matching rule has an associated method for comparing values, depending on whether case is to be considered significant when values are being compared. When new attribute types are defined, it may also be necessary to define a new way of comparing values. Extensible matching also allows language-specific matching rules to be defined so that values in languages other than English can be meaningfully compared.

As an added benefit, extensible matching allows you to specify that the attributes that make up the DN of the entry should be searched. For example, using extensible matching you can locate all the entries in the directory that contain the attribute value assertion ou=Engineering anywhere in their DN. Without extensible matching, it's not possible to search on the individual components of the DN.

The syntax of an extensible matching filter is a bit complicated. It consists of five parts, three of which are optional:

  1. An attribute name. If omitted, any attribute type that supports the given matching rule is compared against the value.

  2. The optional string :dn , which indicates that the attributes forming the entry's DN are to be treated as attributes of the entry during the search.

  3. An optional colon and matching-rule identifier that identifies the particular matching rule to be used. If no matching rule is provided, the default matching rule for the attribute being searched should be used. If the attribute name is omitted, the colon and matching rule must be present.

  4. The literal string ":=" , used to separate the matching-rule identifier from the attribute value.

  5. An attribute value to be compared against.

Formally, the grammar for the extensible search filter is

  attr  [":dn"] [":"  matchingrule  ] ":="  value  

where

  • attr is an attribute name.

  • matchingrule is usually given by an object identifier (OID), although if a descriptive name has been assigned to the matching rule, that may be used as well. The OIDs of the matching rules supported by your directory server will be given in its documentation.

  • value is an attribute value to be used for comparison.

Although LDAP largely does away with the mandatory use of OIDs, you will see them from time to time, especially if you use extensible matching rules or if you design your own schema extensions. The topic of extending your directory schema is discussed in Chapter 8, Schema Design. Let's look at some examples of extensible matching filters:

  • The following filter specifies that all entries in which the cn attribute matches the value Barbara Jensen should be returned:

     (cn:1.2.3.4.5.6:=Barbara Jensen) 

    When comparing values, the matching rule given by the OID 1.2.3.4.5.6 should be used.

  • The following filter specifies that all entries that contain the string jensen in the surname should be returned:

     (sn:dn:1.2.3.4.5.7:=jensen) 

    Object Identifiers

    Object identifiers, commonly referred to as OIDs, are unique identifiers assigned to objects. They are used to uniquely identify many different types of things, such as X.500 directory object and attribute types. In fact, just about everything in the X.500 directory system is identified by an OID. OIDs are also used to uniquely identify objects in other protocols, such as the Simple Network Management Protocol (SNMP).

    OIDs are written as strings of dotted decimal numbers . Each part of an OID represents a node in a hierarchical OID tree. This hierarchy allows an arbitrarily large number of objects to be named, and it supports delegation of the namespace. For example, all the user attribute types defined by the X.500 standards begin with "2.5.4". The cn attribute is assigned the OID 2.5.4.3 , and the sn attribute is assigned the OID 2.5.4.4 .

    An individual subtree of the OID tree is called an arc . Individual arcs may be assigned to organizations, which can then further divide the arc into subarcs , if so desired. For example, Netscape Communications Corporation has been assigned an arc of the OID namespace for its own use. Internally, it has divided that arc into subarcs for use by the various product teams . Delegating the management of the OID namespace in this fashion can prevent conflicts.

    The X.500 protocol makes extensive use of OIDs to uniquely identify various protocol elements. LDAP, on the other hand, favors short, textual names for things: cn to describe the common name attribute and person to identify the person object class, for example. To maintain compatibility with X.500, LDAP allows a string representation of an OID to be used interchangeably with the short name for the item. For example, the search filters (cn=Barbara Jensen) and (2.5.4.3=Barbara Jensen) are equivalent. Unless you're working with an LDAP-based gateway into an X.500 system, you should generally avoid using OIDs in your directory-enabled applications.

    The sn attributes within the DN are also searched. When comparing values, the matching rule given by the OID 1.2.3.4.5.7 should be used.

  • The following filter returns any entries in which the o (organization) attribute exactly matches Example and any entry in which o=Example is one of the components of the DN:

     (o:dn:=Example) 
  • The following filter returns any entries in which a DN component with a syntax appropriate to the given matching rule matches Example :

     (:dn:1.2.3.4.5.8:=Example) 

    The matching rule given by the OID 1.2.3.4.5.8 should be used.

Negation

Any search element can be negated if the filter is preceded with an exclamation point ( ! ). For example, the filter (!(sn=Smith)) matches all entries in which the sn attribute does not contain the value smith , including entries with no sn attribute at all.

Combining Filter Terms

Filters can also be combined by AND and OR operators. The AND operator is signified by an ampersand ( & ), and the OR operator is signified by the vertical bar ( ). When combining search filters, you use prefix notation , in which the operator precedes its arguments. Those familiar with the "reverse polish notation" common on Hewlett-Packard calculators will be familiar with this concept (although reverse polish is a postfix notation, not a prefix notation like that used in LDAP search filters).

Let's look at some examples of combinations of LDAP search filters. The filter (&(sn=Smith) (L=Mountain View)) matches all entries with a surname of smith that also have an L (locality) attribute of Mountain View . In other words, this filter finds everyone named Smith in the Mountain View location. The filter ((sn=Smith) (sn=Jones)) matches everyone with a surname of Smith or Jones .

You use parentheses to group more complex filters to make the meaning of the filter unambiguous. For example, if you want to search the directory for all entries that have an e-mail address but do not have a telephone number, you use the following filter:

 (&(mail=*)(!(telephoneNumber=*))) 

Note that the parentheses bind the negation operator to the presence filter for telephone number.

Technically speaking, parentheses are always required, even if the filter consists of only a single term. Some LDAP software allows you to omit the enclosing parentheses and inserts them for you before sending the search request to the server. However, if you are developing your own software using one of the available SDKs, you need to include the enclosing parentheses.

Table 2.3 summarizes the six types of search filters and the three Boolean operators.

Escaping in Search Filters

If you need to search for an attribute value that contains one of five specific characters, you need to substitute the character with an escape sequence consisting of a backslash and a two-digit hexadecimal sequence representing the character's value. Table 2.4 shows the characters that must be escaped, along with the escape sequence you should use for each. For example, to search for all entries in which the cn attribute exactly matches the value A*Star , you use the filter (cn=A\2AStar) .

Note that the rules for escaping search filters and the rules for escaping distinguished names are different and not interchangeable.

Table 2.3. Types of LDAP Search Filters

Filter Type

Format

Example

Matches

Equality

(attr=value)

(sn=jensen)

Surnames exactly equal to jensen

Substring

(attr=[leading]*[any]*[trailing])

(sn=*jensen*)

Surnames containing the string "jensen"

   

(sn=jensen*)

Surnames starting with the string "jensen"

   

(sn=*jensen)

Surnames ending with the string "jensen"

   

(sn=jen*s*en)

Surnames starting with "jen", containing an "s", and ending with "en"

Approximate

(attr~=value)

(sn~=jensin)

Surnames approximately equal to jensin (for example, surnames that sound like "jensin" ”note the misspelling)

Greater than or equal to

(attr>=value)

(sn>=Jensen)

Surnames lexicographically greater than or equal to Jensen

Less than or equal to

(attr<=value)

(sn<=Jensen)

Surnames lexicographically less than or equal to Jensen

Presence

(attr=*)

(sn=*)

All surnames

AND

(&(filter1)(filter2)...))

(&(sn=Jensen)(objectclass=person))

Entries with an object class of person and surname exactly equal to Jensen

OR

((filter1)(filter2)...))

((sn~=Jensin)(sn=*jensin))

Entries with a surname approximately equal to Jensin or a surname ending in "jensin"

NOT

(!(filter)

(!(mail=*))

All entries without a mail attribute

Table 2.4. Characters That Must Be Escaped If Used in a Search Filter

Character Sequence

Decimal Value

Hex Value

Escape Sequence

* (asterisk)

42

0x2A

\2A

( (left parenthesis)

40

0x28

\28

) (right parenthesis)

41

0x29

\29

\ (backslash)

92

0x5c

\5C

NUL (the null byte)

0x00

\00

Common Types of Searches

Although the LDAP search operation is flexible, some types of searches you'll use more frequently than others:

  • Retrieving a single entry . To retrieve a particular directory entry, use a scope of base , a search base equal to the DN of the entry you want to retrieve, and a filter of (objectclass=*) . The filter, which is a presence filter on the objectclass attribute, will match any entry that contains at least one value in its objectclass attribute. Because every entry in the directory must have an objectclass attribute, this filter is guaranteed to match any directory entry. Because you've specified a scope of base , only one entry will be returned by the search (if the entry exists at all). This is how you use the search operation to read a particular entry.

  • Listing all entries directly below an entry . To list all the directory entries at a particular level in the tree, use the same filter (objectclass=*) as when retrieving a particular entry, but use a scope of onelevel and a search base equal to the DN just above the level you want to list. All the entries immediately below the search base entry will be returned. The search base entry itself is not returned in a onelevel search. (The search base entry is returned in a base or sub search if it matches the search filter.)

  • Searching for matching entries within a subtree . Another common search operation looks within a subtree of the directory for all entries that match particular search criteria. To perform this type of search, use a filter that selects the entries you're interested in retrieving ”or (objectclass=*) if you want all entries ”along with a scope of sub and a search base equal to the DN of the entry at the top of the tree you want to search.

Hiding LDAP Filters from Users

You might justifiably be thinking that your users will never be able to understand LDAP filter syntax. The prefix notation it uses is hardly intuitive, after all! Bear in mind, though, that any good directory access GUI hides the details of filter construction from end users.

Instead of requiring users to type raw LDAP filters, a set of pop-up menus and text boxes is typically used to allow the user to specify the search criteria, and the GUI client constructs the filter for the user. In Figure 2.15, for example, Netscape Communicator's Search window uses the provided information to construct the filter (&(cn=*Smith*) (L=*Dearborn*)) .

Figure 2.15. A GUI Interface for Searching the Directory

If you are a directory administrator, it's a good idea to become familiar with LDAP filter syntax. You can use this knowledge to provide complex "canned" queries for your end users, for example. Filter syntax also crops up in LDAP URLs and configuration files. Spending a little time understanding filter syntax is well worth the effort.

The Compare Operation

The second of the two interrogation operations, the LDAP compare operation, is used to check whether a particular entry contains a particular attribute value. The client submits a compare request to the server, supplying a DN, an attribute name, and a value. The server returns an affirmative response to the client if the entry named by the DN contains the given value in the given attribute type. If not, a negative response is returned.

It may seem odd that the compare operation even exists. After all, if you want to determine whether a particular entry contains a particular attribute value, you can just perform a search with a search base equal to the DN of the entry, a scope of base , and a filter expressing the test you want to conduct. If the entry is returned, the test was successful; if no entry is returned, the test was not successful.

The reasons that the compare operation exists are mainly historical and related to LDAP's roots in X.500. In only one case do the compare and search operations behave differently. If a comparison is attempted on an attribute but the attribute is not present in the entry, the compare operation returns a special indication to the client that the attribute does not exist. The search operation, on the other hand, simply does not return the entry in such cases. This capability to distinguish between "the entry has the attribute but contains no matching value" and "the entry does not have the attribute at all" may be convenient in some situations. The other advantage of the compare operation is that it is more compact in terms of the number of protocol bytes exchanged between the client and the server.

The LDAP Update Operations

LDAP has four update operations: add, delete, rename (modify DN), and modify. These four operations define the ways that you can manipulate the data in your directory.

The Add Operation

The add operation allows you to create new directory entries. It has two parameters: the distinguished name of the entry to be created, and a set of attributes and attribute values that will constitute the new entry. For the add operation to complete successfully, four conditions must be met:

  1. The parent of the new entry must already exist in the directory.

  2. There must not be an entry of the same name.

  3. The new entry must conform to the schema that is in effect.

  4. Access control must permit the operation.

If all these conditions are met, the new entry is added to the directory.

The Delete Operation

The delete operation removes an entry from the directory. It has a single parameter: the DN of the entry to be deleted. For the delete operation to complete successfully, three conditions must be met:

  1. The entry to be deleted must exist.

  2. The entry to be deleted must have no children.

  3. Access control must permit the entry to be deleted.

If all these conditions are met, the entry is removed from the directory.

The Rename (Modify DN) Operation

The rename, or modify DN, operation is used to rename and/or move entries in the directory. It has four parameters: the DN of the entry to be renamed , the new RDN for the entry, an optional argument giving the new parent of the entry, and the delete-old-RDN flag. For the modify DN operation to succeed, the following conditions must be met:

  • The entry being renamed must exist.

  • The new name for the entry must not already be in use by another entry.

  • Access control must permit the operation.

If all these conditions are met, the entry is renamed and/or moved.

If the entry is to be renamed but will still have the same parent entry, the new-parent argument is left blank. Otherwise, the new-parent argument gives the DN of the container where the entry is to be moved. The delete-old-RDN flag is a Boolean flag that specifies whether the old RDN of the entry is to be retained as an attribute of the entry or removed. Figures 2.16 through 2.20 show the various combinations of renaming and moving entries that can be performed with the modify DN operation.

Figure 2.16. Renaming an Entry without Moving It

Figure 2.20. Renaming an Entry, deleteoldrdn=false

LDAPv2 did not have a modify DN operation; it had only a modify RDN operation. As the name implies, modify RDN allows only the RDN of an entry to be changed. This means that an LDAPv2 server may rename an entry but may not move it to a new location in the tree. To accomplish a move with LDAPv2, you must copy the entry, along with any child entries underneath it, to the new location in the tree and delete the original entry or entries.

Figure 2.17. Moving an Entry without Changing Its RDN

Figure 2.18. Moving an Entry and Changing Its RDN Simultaneously

The Modify Operation

The modify operation allows you to update an existing directory entry. It takes two parameters: the DN of the entry to be modified and a set of modifications to be applied. These modifications can specify that new attribute values are to be added to the entry, that specific attribute values are to be deleted from the entry, or that all attribute values for a given attribute are to be replaced with a new set of attribute values. The modify request can include as many attribute modifications as needed.

For the modify operation to succeed, the following conditions must be met:

  • The entry to be modified must exist.

  • All the attribute modifications must succeed.

    Figure 2.19. Renaming an Entry, deleteoldrdn=true

  • The resulting entry must obey the schema that is in effect.

  • Access control must allow the update.

If all these conditions are met, the entry is modified. Note that all the modifications must succeed, or else the entire operation fails and the entry is not modified. This requirement prevents inconsistencies that might arise from half-completed modify operations.

This last point raises one additional but important topic about the LDAP update operations: Each operation is atomic , meaning that the whole operation is processed as a single unit of work. Either this unit completely succeeds, or no modifications are performed. For example, a modify request that affects multiple attributes within an entry cannot half-succeed, with certain attributes updated and others not updated. If the client receives a success result from the server, then all the modifications were applied to the entry. If the server returns an error to the client, then none of the modifications were applied.

The LDAP Authentication and Control Operations

LDAP has two authentication operations (bind and unbind) and one control operation (abandon).

The Bind Operation

By providing a DN and a set of credentials, a client can use the bind operation to authenticate itself to the directory. The server checks whether the credentials are correct for the given DN and, if they are, notes that the client is authenticated as long as the connection remains open or until the client reauthenticates. The server can grant privileges to the client on the basis of its identity.

There are several different types of bind methods . In a simple bind, the client presents a DN and a password in cleartext to the LDAP server. The server verifies that the password matches the password value stored in the userPassword attribute of the entry and, if so, returns a success code to the client.

The simple bind does send the password over the network to the server in the clear. However, you can protect against eavesdroppers intercepting passwords by encrypting the connections using Secure Sockets Layer (SSL) or TLS, which are discussed in the next section, The LDAP Security Model.

LDAPv3 also includes a new type of bind operation: the SASL bind. SASL is an extensible, protocol-independent framework for performing authentication and negotiation of security parameters. With SASL, the client specifies the type of authentication protocol it wants to use. If the server supports the authentication protocol, the client and server perform the agreed-on protocol.

For example, the client could specify that it wants to authenticate using the DIGEST-MD5 SASL mechanism. If the server implements DIGEST-MD5 (all LDAPv3-compliant servers must), it constructs a challenge and sends it to the client. The client computes the response to the challenge and sends the response to the server, which verifies the response and returns a final result to the client. DIGEST-MD5 authentication, if properly implemented, is immune to eavesdroppers.

Incorporation of SASL into LDAPv3 means that new authentication methods, such as smart cards or biometric authentication, can be easily implemented for LDAP without the protocol having to be revised.

It's perfectly legal for a client to bind, perform some operations, bind again, and perform more operations. If it does, all operations performed after the client rebinds are performed with the new bind identity. In the event that a bind operation fails, the client is treated as if it has bound anonymously. We discuss anonymous binds shortly, in the section titled The LDAP Security Model.

Note

In the Active Directory Services Interface (ADSI) SDK, the meaning of the word bind is slightly different. When you bind to a directory entry using ADSI, you not only authenticate to the directory; you also set the base object for subsequent directory searches.


The Unbind Operation

The second authentication operation is the unbind operation. The unbind operation has no parameters. When a client issues an unbind operation, the server discards any authentication information it has associated with the client's connection, terminates any outstanding LDAP operations, and disconnects from the client, thus closing the TCP connection.

Although it's considered good practice for a client to issue an unbind operation before disconnecting, server implementations must behave properly if the client disconnects without unbinding.

The Abandon Operation

The abandon operation has a single parameter: the message ID of the LDAP operation to abandon. The client issues an abandon operation when it is no longer interested in obtaining the results of a previously initiated operation. Upon receiving an abandon request, the server terminates processing of the operation that corresponds to the message ID. The abandon request, typically used by GUI clients, is sent when the user cancels a long-running search request.

Note that it's possible for the abandon request (coming from the client) and the results of the abandoned operation (going to the client) to pass each other in flight. The client needs to be prepared to receive (and discard) results from operations that it has abandoned but that the server sent anyway. If you are using an LDAP SDK, however, you don't need to worry about this; the SDK takes care of this housekeeping for you.

The LDAP Security Model

We've discussed three of the four LDAP models so far. We have a set of directory entries, which are arranged into a hierarchy, and a set of protocol operations that allow us to authenticate to, search, and update the directory. All that remains is to provide a framework for protecting the information in the directory from unauthorized access. This is the purpose of the LDAP security model.

The security model relies on the fact that LDAP is a connection-oriented protocol. In other words, an LDAP client opens a connection to an LDAP server and performs various protocol operations on the same connection. The LDAP client may authenticate to the directory server at some point during the lifetime of the connection, at which point it may be granted additional (or fewer) privileges. For example, a client might authenticate as a particular identity that has been granted read/write access to all the entries in the directory. Before this authentication, it has a limited set of privileges (usually a default set of privileges extended to all users of the directory). After it authenticates, however, it is granted expanded privileges as long as the connection remains open.

What exactly is authentication ? From the client's perspective, it is the process of proving to the server that the client is a particular entity. In other words, the client asserts that it has a certain identity and provides some credentials to prove this assertion. From the server's perspective, the process of authentication involves accepting the identity and credentials provided by the client and checking whether they prove that the client is who it claims to be.

To illustrate this abstract concept with a concrete example, let's examine how LDAP simple authentication works. In simple authentication , an LDAP client provides to an LDAP server a DN and a password, which are sent to the server in the clear (not hashed or encrypted in any way). The server locates the entry in the directory corresponding to the DN provided by the client and checks whether the password presented by the client matches the value stored in the userPassword attribute of the entry. If it does, the client is authenticated; if it does not, the authentication operation fails and an error code is returned to the client.

Note

In Netscape Directory Server, a hashed version of the password can be stored instead of the cleartext password. The hash is computed with one of several cryptographic one-way hash algorithms. When servicing a simple bind operation, the server takes the cleartext password provided by the client in the bind operation, hashes it, and compares it to the hashed password stored in the database. If the hashes match, the bind operation succeeds. Because the hash operation is one-way, it is difficult to determine the password if given only the hash. Storing passwords in hashed form improves the security of your directory somewhat.


The process of authenticating to the directory is called binding . An identity is bound to the connection when the bind operation achieves a successful authentication. If a client does not authenticate, or if it authenticates without providing any credentials, the client is bound anonymously. In other words, the server has no idea who the client is, so it grants a default set of privileges to the client. Usually this default set of privileges is minimal. In some instances, the default set of privileges is completely restrictive: No part of the directory may be read or searched. How you treat anonymously bound clients is up to the directory administrator and depends on the security policy appropriate to your organization. You can find more information on security and privacy in Chapter 12, Privacy and Security Design.

Many different types of authentication systems are independent of LDAP. LDAPv2 supported only simple authentication, in which a DN and password are transmitted in the clear from the client to the server.

Note

The statement that LDAPv2 supported only simple authentication is not completely correct because LDAPv2 also supported Kerberos version 4 authentication, which does not require that passwords be sent in the clear. However, Kerberos v4 was not commercially successful and was superseded by Kerberos version 5. Kerberos support was therefore dropped from the core LDAPv3 protocol, although it's entirely feasible to support it via a SASL mechanism.


Acknowledging the need to support many different authentication methods, LDAPv3 has adopted the SASL framework. SASL provides a standard way for multiple authentication protocols to be supported by LDAPv3. Each type of authentication system corresponds to a particular SASL mechanism. A SASL mechanism is an identifier that describes the type of authentication protocol being supported.

After the server verifies the identity of the client, it can choose to grant additional privileges on the basis of a site-specific policy. For example, you might have a policy that, when authenticated, enables users to search the directory but does not enable them to modify their own directory entries. Or you might have a more permissive policy that allows some authenticated users to modify certain attributes of their own entries, whereas other users (your administrative staff) may modify any attribute of any entry. The way you describe the access rights, the entities to which those rights are granted, and the directory entries to which those rights apply are collectively called access control .

LDAPv3 Authentication Methods

During work on LDAPv3, it became clear that it was necessary to define the minimum set of authentication methods that must be supported by LDAPv3 servers. Without such a definition, a client from one vendor and a server from another vendor might not have any authentication methods in common other than simple authentication. Recall that LDAP simple authentication sends passwords in the clear over the network, with no encryption. Allowing such a situation to exist would severely limit LDAP's adoption.

To address this problem, the LDAP Working Group defined authentication methods whose implementation was mandatory in RFC 2829, Authentication Methods for LDAP . In this document, LDAP servers are broken down into three distinct groups, with separate requirements for each:

  1. Read-only public directory servers may allow anonymous authentication (no password).

  2. Servers that support password-based authentication must support the DIGEST-MD5 SASL mechanism documented in RFC 2831, Using Digest Authentication as a SASL Mechanism .

  3. Servers that require session protection (encryption) and authentication must implement the StartTLS extended operation, as defined in RFC 2830, Lightweight Directory Access Protocol (v3): Extension for Transport Layer Security . This extended operation allows an LDAP client to request encryption of all data flowing between it and the server, and it allows the client and server to authenticate each other using public key certificates. The StartTLS extended operation is described in detail shortly, in the section titled Transport Layer Security (TLS).

Of course, any server can use a more secure authentication method than is strictly required. For example, servers that support password-based authentication may also support StartTLS. The intent of RFC 2829 is to ensure that any LDAPv3-compliant client can authenticate to any LDAPv3-compliant server in a secure fashion, without sending passwords in the clear.

Access Control Models

It may come as somewhat of a disappointment to learn that LDAP does not currently define a standard access control model. However, this does not mean that individual LDAP server implementations have no access control model. In fact, any commercially successful server software must have such a model.

Netscape Directory Server, for example, has a rich access control model. The model works by describing what a given identity can do to a particular set of entries, with granularity down to the attribute level. For example, with the Netscape server it is possible to specify an access control instruction (ACI) that allows a person to modify only the description attribute of his or her own entry. Or the model can allow you to grant complete rights to the directory to all persons in a particular group. This approach allows easy creation of a set of directory administrators; a given person's rights can be easily revoked by removal of the person from the group. The model is fully documented in the Netscape Directory Server Administrator's Guide .

The IETF continues to work on defining a standard access control model and a standard syntax for representing access control rights. The promise for the future is that you, as a directory deployer, will be able to deploy directory servers from several vendors and implement a consistent security policy across those servers ”whether they cooperate to serve a distributed directory or are replicas of each other. Unfortunately, that is not the case today. You would be wise to document your access control policy in plain language so that you can adapt it to whatever model and syntax emerge from the standards bodies in the future.

Transport Layer Security (TLS)

TLS is a security technology that supports privacy, data integrity, and encryption for connection-oriented protocols like TCP. Clients that use TLS to communicate with a server

  • Can be confident that communications are immune to eavesdropping

  • Can be confident that communications are immune to tampering (so-called man-in-the-middle attacks)

  • Can authenticate to the server, using a public key certificate

  • Can verify the authenticity of the server to which they have connected, by verifying the server's public key certificate

SSL has been a successful technology for the World Wide Web, securing electronic commerce and other transactions that depend on transmission of data being hidden from eavesdroppers. TLS, the follow-up to SSL, is an emerging Internet standard. LDAP offers a standard way for clients to begin encrypting all data flowing to and from LDAP on the connection using TLS.

Just as SSL and TLS enabled a new class of applications on the Web, they will enable new uses of directory technology. For example, two companies in a trading-partner relationship can allow directory queries from their trading partners to travel over the Internet. Because TLS encrypts these queries and the results, each company can rest assured that the directory data is protected while in transit over the Internet. Figure 2.21 depicts this scenario.

Figure 2.21. TLS Allows Secure Transmission of Directory Data over the Internet

Readers familiar with existing directory server implementations, such as Netscape Directory Server, will point out that there already is a way to use SSL with LDAP. Many server implementations support the use of LDAP-over-SSL, known as LDAPS. Servers that implement LDAPS must provide this service on a TCP port distinct from the normal LDAP service. Typically, a server listens on port 389 for LDAP connections and port 636 for LDAPS connections.

By contrast, TLS allows a client to begin a connection without encryption, and to negotiate encryption and authentication after the connection is established. This means that an LDAP server supporting TLS can support both types of clients (secure and nonsecure) on the same TCP port.

The StartTLS extended operation, defined in RFC 2830, Lightweight Directory Access Protocol (v3): Extension for Transport Layer Security , is the means by which an LDAP client indicates to an LDAP server that TLS should be used on an existing LDAP connection. After an LDAP client initiates TLS, it can bind through use of the the SASL EXTERNAL mechanism. The server typically maps the certificate provided by the client to a directory entry using an implementation-specific method (for example, Netscape Directory Server maps certificates to entries using a method configurable via the certmap . conf configuration file). An LDAP client usually goes through the following steps to establish a secure, authenticated connection to a directory server:

Step 1. Open a TCP connection to the server.

Step 2. Send a StartTLS extended operation. Lower-layer protocols then negotiate encryption and authentication according to the TLS specification.

Step 3. Bind using the SASL EXTERNAL mechanism if a certificate was provided during TLS negotiation, or using another SASL mechanism, such as DIGEST-MD5.

After TLS has been established and a bind operation performed, the client has a secure, authenticated connection to the directory.

   


Understanding and Deploying LDAP Directory Services
Understanding and Deploying LDAP Directory Services (2nd Edition)
ISBN: 0672323168
EAN: 2147483647
Year: 2002
Pages: 242

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net