Introduction to LDAP


In the preceding sections, you have seen how to interact with a relational database. In this section, we briefly look at hierarchical databases that use LDAP, the Lightweight Directory Access Protocol. This section is adapted from Core JavaServer Faces by Geary and Horstmann [Sun Microsystems Press 2004].

LDAP is preferred over relational databases when the application data naturally follows a tree structure and when read operations greatly outnumber write operations. LDAP is most commonly used for the storage of directories that contain data such as user names, passwords, and permissions.

NOTE

For an in-depth discussion of LDAP, we recommend the "LDAP bible": Understanding and Deploying LDAP Directory Services, 2nd ed., by Timothy Howes et al. [Macmillan 2003].


An LDAP database keeps all data in a tree structure, not in a set of tables as a relational database would. Each entry in the tree has the following:

  • Zero or more attributes. An attribute has an ID and a value. An example attribute is cn=John Q. Public. (The ID cn stores the "common name." See Table 4-10 for the meaning of commonly used LDAP attributes.)

    Table 4-10. Commonly Used LDAP Attributes

    Attribute ID

    Meaning

    dc

    Domain component

    cn

    Common name

    sn

    Surname

    dn

    Distinguished name

    o

    Organization

    ou

    Organizational unit

    uid

    Unique identifier


  • One or more object classes. An object class defines the set of required and optional attributes for this element. For example, the object class person defines a required attribute cn and an optional attribute telephoneNumber. Of course, the object classes are different from Java classes, but they also support a notion of inheritance. For example, organizationalPerson is a subclass of person with additional attributes.

  • A distinguished name (for example, uid=jqpublic,ou=people,dc=mycompany,dc=com). The distinguished name is a sequence of attributes that trace a path joining the entry with the root of the tree. There may be alternate paths, but one of them must be specified as distinguished.

Figure 4-9 shows an example of a directory tree.

Figure 4-9. A directory tree


How to organize a directory tree, and what information to put in it, can be a matter of intense debate. We do not discuss the issues here. Instead, we simply assume that an organizational scheme has been established and that the directory has been populated with the relevant user data.

Configuring an LDAP Server

You have several options for running an LDAP server to try out the programs in this section. Here are the most common choices:

  • IBM Tivoli Directory Server

  • Microsoft Active Directory

  • Novell eDirectory

  • OpenLDAP (http://openldap.org), a free server available for Linux and Windows and built into Mac OS X

  • Sun Java System Directory Server

We give you brief instructions for configuring OpenLDAP. If you use another directory server, the basic steps are similar.

If you use OpenLDAP, you need to edit the slapd.conf file before starting the LDAP server. (On Linux, the default location for the slapd.conf file is /usr/local/etc/openldap.) Edit the suffix entry in slapd.conf to match the sample data set. This entry specifies the distinguished name suffix for this server. It should read

 suffix  "dc=mycompany,dc=com" 

You also need to configure an LDAP user with administrative rights to edit the directory data. In OpenLDAP, add these lines to slapd.conf:

 rootdn  "cn=Manager,dc=mycompany,dc=com" rootpw  secret 

You can now start the LDAP server. On Linux, run /usr/local/libexec/slapd.

Next, populate the server with the sample data. Most LDAP servers allow the import of LDIF (Lightweight Directory Interchange Format) data. LDIF is a human-readable format that simply lists all directory entries, including their distinguished names, object classes, and attributes. Example 4-6 shows an LDIF file that describes our sample data.

Example 4-6. sample.ldif
  1. # Define top-level entry  2. dn: dc=mycompany,dc=com  3. objectClass: dcObject  4. objectClass: organization  5. dc: mycompany  6. o: Core Java Team  7.  8. # Define an entry to contain people  9. # searches for users are based on this entry 10. dn: ou=people,dc=mycompany,dc=com 11. objectClass: organizationalUnit 12. ou: people 13. 14. # Define a user entry for John Q. Public 15. dn: uid=jqpublic,ou=people,dc=mycompany,dc=com 16. objectClass: person 17. objectClass: uidObject 18. uid: jqpublic 19. sn: Public 20. cn: John Q. Public 21. telephoneNumber: +1 408 555 0017 22. userPassword: wombat 23. 24. # Define a user entry for Jane Doe 25. dn: uid=jdoe,ou=people,dc=mycompany,dc=com 26. objectClass: person 27. objectClass: uidObject 28. uid: jdoe 29. sn: Doe 30. cn: Jane Doe 31. telephoneNumber: +1 408 555 0029 32. userPassword: heffalump 33. 34. # Define an entry to contain LDAP groups 35. # searches for roles are based on this entry 36. dn: ou=groups,dc=mycompany,dc=com 37. objectClass: organizationalUnit 38. ou: groups 39. 40. # Define an entry for the "techstaff" group 41. dn: cn=techstaff,ou=groups,dc=mycompany,dc=com 42. objectClass: groupOfUniqueNames 43. cn: techstaff 44. uniqueMember: uid=jdoe,ou=people,dc=mycompany,dc=com 45. 46. # Define an entry for the "staff" group 47. dn: cn=staff,ou=groups,dc=mycompany,dc=com 48. objectClass: groupOfUniqueNames 49. cn: staff 50. uniqueMember: uid=jqpublic,ou=people,dc=mycompany,dc=com 51. uniqueMember: uid=jdoe,ou=people,dc=mycompany,dc=com 

For example, with OpenLDAP, you use the ldapadd tool to add the data to the directory:

 ldapadd -f sample.ldif -x -D "cn=Manager,dc=mycompany,dc=com" -w secret 

Before proceeding, it is a good idea to double-check that the directory contains the data that you need. We suggest that you download Jarek Gawor's LDAP Browser\Editor from http://www-unix.mcs.anl.gov/~gawor/ldap/. This convenient Java program lets you browse the contents of any LDAP server. Launch the program and configure it with the following options:

  • Host: localhost

  • Base DN: dc=mycompany,dc=com

  • Anonymous bind: unchecked

  • User DN: cn=Manager

  • Append base DN: checked

  • Password: secret

Make sure the LDAP server has started, then connect. If everything is in order, you should see a directory tree similar to that shown in Figure 4-10.

Figure 4-10. Inspecting an LDAP directory tree


Accessing LDAP Directory Information

Once your LDAP database is populated, connect to it with a Java program. You use the Java Naming and Directory Interface (JNDI), an interface that unifies various directory protocols.

Start by getting a directory context to the LDAP directory, with the following incantation:

 Hashtable env = new Hashtable(); env.put(Context.SECURITY_PRINCIPAL, username); env.put(Context.SECURITY_CREDENTIALS, password); DirContext initial = new InitialDirContext(env); DirContext context = (DirContext) initial.lookup("ldap://localhost:389"); 

Here, we connect to the LDAP server at the local host. The port number 389 is the default LDAP port.

If you connect to the LDAP database with an invalid user/password combination, an AuthenticationException is thrown.

NOTE

Sun's JNDI tutorial suggests an alternative way to connect to the server:

 Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389"); env.put(Context.SECURITY_PRINCIPAL, userDN); env.put(Context.SECURITY_CREDENTIALS, password); DirContext context = new InitialDirContext(env); 

However, it seems undesirable to hardwire the Sun LDAP provider into your code. JNDI has an elaborate mechanism for configuring providers, and you should not lightly bypass it.


To list the attributes of a given entry, specify its distinguished name and then use the getAttributes method:

 Attributes attrs = context.getAttributes("uid=jqpublic,ou=people,dc=mycompany,dc=com"); 

You can get a specific attribute with the get method, for example,

 Attribute commonNameAttribute = attrs.get("cn"); 

To enumerate all attributes, you use the NamingEnumeration class. The designers of this class felt that they too could improve on the standard Java iteration protocol, and they gave us this usage pattern:

 NamingEnumeration<? extends Attribute> attrEnum = attrs.getAll(); while (attrEnum.hasMore()) {     Attribute attr = attrEnum.next();     String id = attr.getID();     . . . } 

Note the use of hasMore instead of hasNext.

If you know that an attribute has a single value, you can call the get method to retrieve it:

 String commonName = (String) commonNameAttribute.get(); 

If an attribute can have multiple values, you need to use another NamingEnumeration to list them all:

 NamingEnumeration<?> valueEnum = attr.getAll(); while (valueEnum.hasMore()) {     Object value = valueEnum.next();     . . . } 

NOTE

As of JDK 5.0, NamingEnumeration is a generic type. The type bound <? extends Attribute> means that the enumeration yields objects of some unknown type that is a subtype of Attribute. Therefore, you don't need to cast the value that next returnsit has type Attribute. Without generics, you would write

 NamingEnumeration attrEnum = attrs.getAll(); Attribute attr = (Attribute) attrEnum.next(); 

However, a NamingEnumeration<?> has no idea what it enumerates. Its next method returns an Object.


You now know how to query the directory for user data. Next, let us take up operations for modifying the directory contents.

To add a new entry, gather the set of attributes in a BasicAttributes object. (The BasicAttributes class implements the Attributes interface.)

 Attributes attrs = new BasicAttributes(); attrs.put("uid", "alee"); attrs.put("sn", "Lee"); attrs.put("cn", "Amy Lee"); attrs.put("telephoneNumber", "+1 408 555 0033"); String password = "redqueen"; attrs.put("userPassword", password.getBytes()); // the following attribute has two values Attribute objclass = new BasicAttribute("objectClass"); objclass.add("uidObject"); objclass.add("person"); attrs.put(objclass); 

Then call the createSubcontext method. Provide the distinguished name of the new entry and the attribute set.

 context.createSubcontext("uid=alee,ou=people,dc=mycompany,dc=com", attrs); 

CAUTION

When assembling the attributes, remember that the attributes are checked against the schema. Don't supply unknown attributes, and be sure to supply all attributes that are required by the object class. For example, if you omit the sn of person, the createSubcontext method will fail.


To remove an entry, call destroySubcontext:

 context.destroySubcontext("uid=jdoe,ou=people,dc=mycompany,dc=com"); 

Finally, you may want to edit the attributes of an existing entry. You call the method

 context.modifyAttributes(distinguishedName, flag, attrs); 

Here, flag is one of

 DirContext.ADD_ATTRIBUTE DirContext.REMOVE_ATTRIBUTE DirContext.REPLACE_ATTRIBUTE 

The attrs parameter contains a set of the attributes to be added, removed, or replaced.

Conveniently, the BasicAttributes(String, Object) constructor constructs an attribute set with a single attribute. For example,

 context.modifyAttributes("uid=alee,ou=people,dc=mycompany,dc=com",    DirContext.ADD_ATTRIBUTE,    new BasicAttributes("title", "CTO")); context.modifyAttributes("uid=alee,ou=people,dc=mycompany,dc=com",    DirContext.REMOVE_ATTRIBUTE,    new BasicAttributes("telephoneNumber", "+1 408 555 0033")); context.modifyAttributes("uid=alee,ou=people,dc=mycompany,dc=com",    DirContext.REPLACE_ATTRIBUTE,    new BasicAttributes("userPassword", password.getBytes())); 

Finally, when you are done with a context, you should close it:

 context.close(); 

The program in Example 4-7 demonstrates how to access a hierarchical database through LDAP. The program lets you view, modify, and delete information in a database with the sample data in Example 4-6.

Enter a uid into the text field and click the Find button to find an entry. If you edit the entry and click Save, your changes are saved. If you edited the uid field, a new entry is created. Otherwise, the existing entry is updated. You can also delete the entry by clicking the Delete button (see Figure 4-11).

Figure 4-11. Accessing a hierarchical database


The following steps briefly describe the program.

1.

The configuration for the LDAP server is contained in the file ldapserver.properties. The file defines the URL, user name, and password of the server, like this:

 ldap.username=cn=Manager,dc=mycompany,dc=com ldap.password=secret ldap.url=ldap://localhost:389 

The getContext method reads the file and obtains the directory context.

2.

When the user clicks the Find button, the findEntry method fetches the attribute set for the entry with the given uid. The attribute set is used to construct a new DataPanel.

3.

The DataPanel constructor iterates over the attribute set and adds a label and text field for each ID/value pair.

4.

When the user clicks the Delete button, the deleteEntry method deletes the entry with the given uid and discards the data panel.

5.

When the user clicks the Save button, the DataPanel constructs a BasicAttributes object with the current contents of the text fields. The saveEntry method checks whether the uid has changed. If the user edited the uid, a new entry is created. Otherwise, the modified attributes are updated. The modification code is simple because we have only one attribute with multiple values, namely, objectClass. In general, you would need to work harder to handle multiple values for each attribute.

6.

Similar to the program in Example 4-4, we close the directory context when the frame window is closing.

You now know enough about directory operations to carry out the tasks that you will commonly need when working with LDAP directories. A good source for more advanced information is the JNDI tutorial at http://java.sun.com/products/jndi/tutorial.

Example 4-7. LDAPTest.java
   1. import java.net.*;   2. import java.awt.*;   3. import java.awt.event.*;   4. import java.io.*;   5. import java.util.*;   6. import javax.naming.*;   7. import javax.naming.directory.*;   8. import javax.swing.*;   9.  10. /**  11.    This program demonstrates access to a hierarchical database through LDAP  12. */  13. public class LDAPTest  14. {  15.    public static void main(String[] args)  16.    {  17.       JFrame frame = new LDAPFrame();  18.       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  19.       frame.setVisible(true);  20.    }  21. }  22.  23. /**  24.    The frame that holds the data panel and the navigation buttons.  25. */  26. class LDAPFrame extends JFrame  27. {  28.    public LDAPFrame()  29.    {  30.       setTitle("LDAPTest");  31.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);  32.  33.       JPanel northPanel = new JPanel();  34.       northPanel.setLayout(new java.awt.GridLayout(1, 2, 3, 1));  35.       northPanel.add(new JLabel("uid", SwingConstants.RIGHT));  36.       uidField = new JTextField();  37.       northPanel.add(uidField);  38.       add(northPanel, BorderLayout.NORTH);  39.  40.       JPanel buttonPanel = new JPanel();  41.       add(buttonPanel, BorderLayout.SOUTH);  42.  43.       findButton = new JButton("Find");  44.       findButton.addActionListener(new  45.          ActionListener()  46.          {  47.             public void actionPerformed(ActionEvent event)  48.             {  49.                findEntry();  50.             }  51.          });  52.       buttonPanel.add(findButton);  53.  54.       saveButton = new JButton("Save");  55.       saveButton.addActionListener(new  56.          ActionListener()  57.          {  58.             public void actionPerformed(ActionEvent event)  59.             {  60.                saveEntry();  61.             }  62.          });  63.       buttonPanel.add(saveButton);  64.  65.       deleteButton = new JButton("Delete");  66.       deleteButton.addActionListener(new  67.          ActionListener()  68.          {  69.             public void actionPerformed(ActionEvent event)  70.             {  71.                deleteEntry();  72.             }  73.          });  74.       buttonPanel.add(deleteButton);  75.  76.       addWindowListener(new  77.          WindowAdapter()  78.          {  79.             public void windowClosing(WindowEvent event)  80.             {  81.                try  82.                {  83.                   if (context != null) context.close();  84.                }  85.                catch (NamingException e)  86.                {  87.                   e.printStackTrace();  88.                }  89.             }  90.         });  91.    }  92.  93.    /**  94.       Finds the entry for the uid in the text field.  95.    */  96.    public void findEntry()  97.    {  98.       try  99.       { 100.          if (scrollPane != null) remove(scrollPane); 101.          String dn = "u,ou=people,dc=mycompany,dc=com"; 102.          if (context == null) context = getContext(); 103.          attrs = context.getAttributes(dn); 104.          dataPanel = new DataPanel(attrs); 105.          scrollPane = new JScrollPane(dataPanel); 106.          add(scrollPane, BorderLayout.CENTER); 107.          validate(); 108.          uid = uidField.getText(); 109.       } 110.       catch (NamingException e) 111.       { 112.          JOptionPane.showMessageDialog(this, e); 113.       } 114.       catch (IOException e) 115.       { 116.          JOptionPane.showMessageDialog(this, e); 117.       } 118.    } 119. 120.    /** 121.       Saves the changes that the user made. 122.    */ 123.    public void saveEntry() 124.    { 125.       try 126.       { 127.          if (dataPanel == null) return; 128.          if (context == null) context = getContext(); 129.          if (uidField.getText().equals(uid)) // update existing entry 130.          { 131.             String dn = "u,ou=people,dc=mycompany,dc=com"; 132.             Attributes editedAttrs = dataPanel.getEditedAttributes(); 133.             NamingEnumeration<? extends Attribute> attrEnum = attrs.getAll(); 134.             while (attrEnum.hasMore()) 135.             { 136.                Attribute attr = attrEnum.next(); 137.                String id = attr.getID(); 138.                Object value = attr.get(); 139.                Attribute editedAttr = editedAttrs.get(id); 140.                if (editedAttr != null && !attr.get().equals(editedAttr.get())) 141.                   context.modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, 142.                      new BasicAttributes(id, editedAttr.get())); 143.             } 144.          } 145.          else // create new entry 146.          { 147.             String dn = "u,ou=people,dc=mycompany,dc=com"; 148.             attrs = dataPanel.getEditedAttributes(); 149.             Attribute objclass = new BasicAttribute("objectClass"); 150.             objclass.add("uidObject"); 151.             objclass.add("person"); 152.             attrs.put(objclass); 153.             attrs.put("uid", uidField.getText()); 154.             context.createSubcontext(dn, attrs); 155.          } 156. 157.          findEntry(); 158.       } 159.       catch (NamingException e) 160.       { 161.          JOptionPane.showMessageDialog(LDAPFrame.this, e); 162.          e.printStackTrace(); 163.       } 164.       catch (IOException e) 165.       { 166.          JOptionPane.showMessageDialog(LDAPFrame.this, e); 167.          e.printStackTrace(); 168.       } 169.    } 170. 171.    /** 172.       Deletes the entry for the uid in the text field. 173.    */ 174.    public void deleteEntry() 175.    { 176.       try 177.       { 178.          String dn = "u,ou=people,dc=mycompany,dc=com"; 179.          if (context == null) context = getContext(); 180.          context.destroySubcontext(dn); 181.          uidField.setText(""); 182.          remove(scrollPane); 183.          scrollPane = null; 184.          repaint(); 185.       } 186.       catch (NamingException e) 187.       { 188.          JOptionPane.showMessageDialog(LDAPFrame.this, e); 189.          e.printStackTrace(); 190.       } 191.       catch (IOException e) 192.       { 193.          JOptionPane.showMessageDialog(LDAPFrame.this, e); 194.          e.printStackTrace(); 195.       } 196.    } 197. 198.    /** 199.       Gets a context from the properties specified in the file ldapserver.properties 200.       @return the directory context 201.    */ 202.    public static DirContext getContext() 203.       throws NamingException, IOException 204.    { 205.       Properties props = new Properties(); 206.       FileInputStream in = new FileInputStream("ldapserver.properties"); 207.       props.load(in); 208.       in.close(); 209. 210.       String url = props.getProperty("ldap.url"); 211.       String username = props.getProperty("ldap.username"); 212.       String password = props.getProperty("ldap.password"); 213. 214.       Hashtable<String, String> env = new Hashtable<String, String>(); 215.       env.put(Context.SECURITY_PRINCIPAL, username); 216.       env.put(Context.SECURITY_CREDENTIALS, password); 217.       DirContext initial = new InitialDirContext(env); 218.       DirContext context = (DirContext) initial.lookup(url); 219. 220.       return context; 221.    } 222. 223.    public static final int DEFAULT_WIDTH = 300; 224.    public static final int DEFAULT_HEIGHT = 200; 225. 226.    private JButton findButton; 227.    private JButton saveButton; 228.    private JButton deleteButton; 229. 230.    private JTextField uidField; 231.    private DataPanel dataPanel; 232.    private Component scrollPane; 233. 234.    private DirContext context; 235.    private String uid; 236.    private Attributes attrs; 237. } 238. 239. /** 240.    This panel displays the contents of a result set. 241. */ 242. class DataPanel extends JPanel 243. { 244.    /** 245.       Constructs the data panel. 246.       @param attributes the attributes of the given entry 247.    */ 248.    public DataPanel(Attributes attrs) throws NamingException 249.    { 250.       setLayout(new java.awt.GridLayout(0, 2, 3, 1)); 251. 252.       NamingEnumeration<? extends Attribute> attrEnum = attrs.getAll(); 253.       while (attrEnum.hasMore()) 254.       { 255.          Attribute attr = attrEnum.next(); 256.          String id = attr.getID(); 257. 258.          NamingEnumeration<?> valueEnum = attr.getAll(); 259.          while (valueEnum.hasMore()) 260.          { 261.             Object value = valueEnum.next(); 262.             if (id.equals("userPassword")) 263.                value = new String((byte[]) value); 264. 265.             JLabel idLabel = new JLabel(id, SwingConstants.RIGHT); 266.             JTextField valueField = new JTextField("" + value); 267.             if (id.equals("objectClass")) 268.                valueField.setEditable(false); 269.             if (!id.equals("uid")) 270.             { 271.                add(idLabel); 272.                add(valueField); 273.             } 274.          } 275.       } 276.    } 277. 278.    public Attributes getEditedAttributes() 279.    { 280.       Attributes attrs = new BasicAttributes(); 281.       for (int i = 0; i < getComponentCount(); i += 2) 282.       { 283.          JLabel idLabel = (JLabel) getComponent(i); 284.          JTextField valueField = (JTextField) getComponent(i + 1); 285.          String id = idLabel.getText(); 286.          String value = valueField.getText(); 287.          if (id.equals("userPassword")) 288.             attrs.put("userPassword", value.getBytes()); 289.          else if (!id.equals("") && !id.equals("objectClass")) 290.             attrs.put(id, value); 291.       } 292.       return attrs; 293.    } 294. } 


 javax.naming.directory.InitialDirContext 1.3 

  • InitialDirContext(Hashtable env)

    constructs a directory context, using the given environment settings. The hash table can contain bindings for Context.SECURITY_PRINCIPAL, Context.SECURITY_CREDENTIALS, and other keyssee the API documentation for the javax.naming.Context interface for details.


 javax.naming.Context 1.3 

  • Object lookup(String name)

    looks up the object with the given name. The return value depends on the nature of this context. It commonly is a subtree context or a leaf object.

  • Context createSubcontext(String name)

    creates a subcontext with the given name. The subcontext becomes a child of this context. All path components of the name, except for the last one, must exist.

  • void destroySubcontext(String name)

    destroys the subcontext with the given name. All path components of the name, except for the last one, must exist.

  • void close()

    closes this context.


 javax.naming.directory.DirContext 1.3 

  • Attributes getAttributes(String name)

    gets the attributes of the entry with the given name.

  • void modifyAttributes(String name, int flag, Attributes modes)

    modifies the attributes of the entry with the given name. The value flag is one of DirContext.ADD_ATTRIBUTE, DirContext.REMOVE_ATTRIBUTE, or DirContext.REPLACE_ATTRIBUTE.


 javax.naming.directory.Attributes 1.3 

  • Attribute get(String id)

    gets the attribute with the given ID.

  • NamingEnumeration<? extends Attribute> getAll()

    yields an enumeration that iterates through all attributes in this attribute set.

  • Attribute put(Attribute attr)

  • Attribute put(String id, Object value)

    add an attribute to this attribute set.


 javax.naming.directory.BasicAttributes 1.3 

  • BasicAttributes(String id, Object value)

    constructs an attribute set that contains a single attribute with the given ID and value.


 javax.naming.directory.Attribute 1.3 

  • String getID()

    gets the ID of this attribute.

  • Object get()

    gets the first attribute value of this attribute if the values are ordered or an arbitrary value if they are unordered.

  • NamingEnumeration<?> getAll()

    yields an enumeration that iterates through all values of this attribute.


 javax.naming.NamingEnumeration<T> 1.3 

  • boolean hasMore()

    returns true if this enumeration object has more elements.

  • T next()

    returns the next element of this enumeration.



    Core JavaT 2 Volume II - Advanced Features
    Building an On Demand Computing Environment with IBM: How to Optimize Your Current Infrastructure for Today and Tomorrow (MaxFacts Guidebook series)
    ISBN: 193164411X
    EAN: 2147483647
    Year: 2003
    Pages: 156
    Authors: Jim Hoskins

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