Monday Nov 16, 2009

Installing Directory Server Enterprise Edition 7.0

Download DSEE free of charge This entry takes you through Directory Server Enterprise Edition installation on Windows Server 2008, showing screen shots taken while installing Directory Service Control Center with Directory Server and Directory Proxy Server on a Windows system. I wrote a similar entry for DSEE 6.0 in March 2007.

Downloading Directory Server Enterprise Edition

You can download Directory Server Enterprise Edition 7.0 from http://www.sun.com/software/products/directory_srvr_ee/get1.jsp.

To install Directory Server Enterprise Edition, get the Windows .zip file, DSEE.7.0.Windows-X86-zip.zip.

Note that DSEE 7.0 processes run as 32-bit applications on Windows systems.

Installing Directory Server Enterprise Edition Software

  1. Unpack the .zip file, which contains the idsktune utility, the Microsoft Visual C++ 2008 Redistributable Package, and the sun-dsee7.zip to unpack.
    Files in distribution
  2. Double-click and install vcredist_x86 if the Microsoft Visual C++ 2008 Redistributable Package is not already installed on the system.
  3. Run the idsktune utility.
    idsktune Output
    Note that if this were a production install, I would need more RAM.
  4. Unpack sun-dsee7.zip where you want to put the software, such as C:\\Program Files\\Sun.
    The .zip contains a dsee7 folder that houses all the files you need to keep.
    Files unpacked

Note that the files are now installed under C:\\Program Files\\Sun\\dsee7\\.

Setting Up Directory Service Control Center Components

Before you can create Directory Server and Directory Proxy Server instances through the DSCC console, you must set up the DSCC console (a web app housed in an application server such as GlassFish, Tomcat, or WebLogic that I expect you to have installed already), and set up the DSCC agent (a local service residing in the Common Agent Container, aka Cacao, that allows you to access servers, in particular to start them when they are stopped, and so forth).

Note that on Windows 2008 if you are not Administrator, you cannot register the DSCC Agent in the Common Agent Container until you set the following Windows registry keys to 0:

HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableInstallerDetection
HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableLUA

The procedure below worked with GlassFish v2.1.

  1. Create the dscc7.war file containing the application to deploy.
    C:\\Program Files\\Sun\\dsee7\\bin>dsccsetup war-file-create
  2. Initialize the DSCC Registry (the Directory Server instance DSCC uses to keep track of managed servers).
    C:\\Program Files\\Sun\\dsee7\\bin>dsccsetup ads-create
    During this step, set and remember the Directory Service Manager password.
    Directory Service Manager is the DSCC user who has credentials to handle all managed servers.
  3. Register the DSCC Agent in the Common Agent Container.
    C:\\Program Files\\Sun\\dsee7\\bin>dsccsetup cacao-reg
    ...
    Cacao will listen on port 21162
    ...
    Note the port number used by the Common Agent Container.
  4. Add the following permissions to the end of the C:\\Program Files\\Sun\\SDK\\domains\\domain1\\config\\server.policy file.
    // Permissions for Directory Service Control Center
    grant codeBase "file:${com.sun.aas.instanceRoot}/applications/j2ee-modules/dscc7/-"
    {
            permission java.security.AllPermission;
    };
  5. Deploy DSCC.
    Select WAR file

    Deploying DSCC war
  6. Login to Directory Service Control Center using the Directory Service Manager password from the previous procedure.
    Login DSCC
  7. At this point, you should see the DSCC home page.DSCC home

Creating a Directory Server Instance

Time to create a directory to hold sample data.

  1. Click Create New Directory Server.
  2. Provide the properties needed to create the server, and click Next.
    (See the screen shot below for hints.)
  3. Accept the self-signed certificate, and click Next.
  4. Accept the Default Settings, and click Next.
  5. Note that the DSCC Agent Port is the port number provided when the Agent was registered in the previous procedure.
    Create DS instance
    The host name in this case is localhost, since the server is running in VirtualBox on a laptop. (Now you know why I did not give it 4GB RAM ;-)
  6. Click Finish.

Before going further, add a dc=example,dc=com suffix, and import sample data into the Directory Server instance just created.

  1. In DSCC under Directory Server > Suffixes, click New Suffix.
  2. Use the example suffix DN, dc=example,dc=com, and click Next.
  3. Skip replication for now, and click Next.
  4. Choose the localhost:1389 server you created, then click Next.
  5. Accept the Default Settings, and click Next.
  6. Accept the Default Database Location, and click Next.
  7. Select , and click Next.
  8. Read the summary, and click Finish.
    Import example data

Creating a Directory Proxy Server Instance

Next, create a proxy that could be used instead of allowing direct access to the directory.

  1. On the DSCC home page, click Create New Proxy Server.
  2. Provide the properties needed to create the server, and click Next.
    (See the screenshot below for hints.)
  3. Accept the Default Settings, and click Next.
  4. Read the summary, and click Finish.
    Create DPS instance

Finally, you can use DSCC to configure the proxy to send requests to the Directory Server instance.

  1. Under the Proxy Servers tab, click localhost:9389.
  2. Under the Routing > Data Sources tab, click New Data Source.
  3. Provide a name, but otherwise accept defaults throughout the Creating New Data Source wizard.
  4. Under the Routing > Data Source Pools tab, click defaultDataSourcePool.
  5. Click Choose Data Sources, and add the new data source you created to the Data Source list.
  6. In each LDAP operation column for the data source, set the weight to 1.
    Data source pool settings
  7. Under Server Operation > Main, click Restart.

Trying It Out

Directory Server Enterprise Edition is delivered with command line tools for accessing the directory service. For example, the ldapsearch tool installed here in C:\\Program Files\\Sun\\dsee7\\dsrk\\bin, allows you to see that the search functionality is working as expected.

First, search Directory Server on port 1389 to verify the data is there. Then, search through Directory Proxy Server on port 9389 to make sure the proxy is properly configured.

Try it out

Note that you can now access Directory Server data through Directory Proxy Server, and that you can manage both servers through Directory Service Control Center.

Sun Directory Server Enterprise Edition 7.0

Download DSEE free of charge Congratulations to the DSEE 7.0 team!

Sun Directory Server Enterprise Edition 7.0 released today. You can download the software for your platform at http://www.sun.com/software/products/directory_srvr_ee/get1.jsp.

The documentation should follow shortly at http://docs.sfbay.sun.com/app/docs/coll/1819.1.

Wednesday Oct 28, 2009

Sun Identity Manager With OpenDS 2.2

Download OpenDS As Ludo mentions in his announcement about RC 1 for OpenDS 2.2, this release adds external changelog access. Directory applications access the changelog to follow what updates are happening to directory data, and so find the changelog useful when aiming to stay synchronized.

Rob Byrne, who has been stitching together big identity deployments for the last few years, writes about how Sun Identity Manager seems to work fine with the OpenDS changelog, just as it does with the Sun Directory Server changelog. Rob hasn't tested every case, but it looks promising.

Monday Oct 19, 2009

Free Webinar: New Sun Role Manager and Sun Directory Server Enterprise Edition

Last Oct. 21, Neil Gandhi and Nick Wooler offered a Webinar for you on improving compliance, access control, and performance with Sun's latest releases of Role Manager and Directory Server Enterprise Edition.

The presentation is available at https://slx.sun.com/limelight/filevault/1179275769/0/RM5_DSEE7_Customer_Webinar_Presentation_v5.pdf.

Some Q&A are covered in an additional file at https://slx.sun.com/limelight/filevault/1179275769/3/DSRM_Q_A.pdf.

Saturday Aug 22, 2009

PHP LDAP StartTLS to OpenDS

Download OpenDSThe OpenDS server on my laptop is using a self-signed cert. This entry briefly describes using StartTLS to avoid sending passwords over the network in clear text, despite having a certificate that does not check out, because like a CA cert it is self-signed (and free as in beer).

This is certainly not for production. Instead here is a workaround for testing with the self-signed cert. Thanks to pataisjsu for posting this link, http://marc.info/?l=php-windows&m=116127873321748&w=2, on the PHP ldap_start_tls() function page.

You set up an ldap.conf file to let StartTLS proceed even if the server certificate does not check out with the client application.

  1. Set up an environment variable that points to ldap.conf.
    C:\\>echo %LDAPCONF%
    C:\\openldap\\sysconf\\ldap.conf
  2. Add one line to the ldap.conf file for the use PHP makes of OpenLDAP to indicate that the client not request or verify the server certificate:
    TLS_REQCERT never
  3. Add new code -- in auth.php and lookup.php if you downloaded the example -- just after the call to ldap_connect() to use StartTLS.
    ldap_set_option($ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3);
    ldap_start_tls($ldapconn) or die("StartTLS failed.");

That's it. Happy testing.

Friday Aug 21, 2009

Basic SSL LDAP Access With Java

Download OpenDS

An earlier entry demonstrating LDAP Basics with Java uses an unsecured LDAP connection even for authentication. Someone who gets the packets could get the passwords when users do simple authentication. So that scheme will not work in some environments.

One way to get around the problem is to enable SSL (LDAPS) on the LDAP server, and then connect over SSL. If you have official certificates recognized by your Java development kit, moving from an unsecured to a secure SSL connection is as easy as changing the LDAP URL you use to connect. For example, if your Java code currently uses this JNDI:

int port = 389; // Default for LDAP
env.put(Context.PROVIDER_URL, "ldap://" + server + ":" + port + "/");

You need only make sure port is pointing to the SSL port, by default 636 instead of 389, and add an s:

int port = 636; // Default for LDAPS
env.put(Context.PROVIDER_URL, "ldaps://" + server + ":" + port + "/");

The OpenDS server on my laptop is using a self-signed cert for testing. I found a blog entry on how to get that cert easily into %JAVA_HOME%\\jre\\lib\\security\\jssecacerts. See http://blogs.sun.com/andreas/entry/no_more_unable_to_find and the InstallCert.java code posted there.

The laptop has hostname FR-MCRAIG-01, so here is how I got the OpenDS server cert into the keystore.

C:\\Program Files\\Java\\jdk1.6.0_10\\jre\\lib\\security>java InstallCert FR-MCRAIG-01:1636
Loading KeyStore C:\\Program Files\\Java\\jdk1.6.0_10\\jre\\lib\\security\\cacerts...
Opening connection to FR-MCRAIG-01:1636...
Starting SSL handshake...

...

Server sent 1 certificate(s):

 1 Subject CN=FR-MCRAIG-01, O=OpenDS Self-Signed Certificate
   Issuer  CN=FR-MCRAIG-01, O=OpenDS Self-Signed Certificate
   sha1    c8 65 09 20 6d cd ce 49 74 79 94 db c5 de c0 9e 88 22 bf 2e
   md5     91 00 22 b5 af af ce 6f 18 4f d0 53 2a 25 25 79

Enter certificate to add to trusted keystore or 'q' to quit: [1]
1

[
[
  Version: V3
  Subject: CN=FR-MCRAIG-01, O=OpenDS Self-Signed Certificate
  Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5

  Key:  Sun RSA public key, 1024 bits
  modulus: 959636547431068463830836243017478926177556680160559342022702329015019
40821365387949638940708183584259305099964566669834719666082227059496757860621285
33579944708236976067614804323567219627938381230112392624635758595410619743902787
1114358806677720711353274033441757994535828245906353046883352879347380951654609
  public exponent: 65537
  Validity: [From: Tue Aug 04 15:35:44 CEST 2009,
               To: Thu Aug 04 15:35:44 CEST 2011]
  Issuer: CN=FR-MCRAIG-01, O=OpenDS Self-Signed Certificate
  SerialNumber: [    4a783930]

]
  Algorithm: [SHA1withRSA]
  Signature:
0000: 60 21 E2 C0 09 D5 11 C8   80 91 55 44 A3 C3 D2 39  `!........UD...9
0010: 37 04 89 76 31 0E 40 9F   11 6D 79 E9 CB 14 1D 63  7..v1.@..my....c
0020: 6B 7B 0E 2A DF 74 18 BD   59 07 44 73 72 C0 D3 EA  k..\*.t..Y.Dsr...
0030: 61 E1 A7 79 90 EE 73 AB   A0 40 FF F7 A9 F5 CA 0B  a..y..s..@......
0040: 6E FE 81 14 E3 1B 5C 50   83 96 9B B7 23 8C 8C ED  n.....\\P....#...
0050: 7D 1C 22 BA DF 20 8D F4   82 8D 72 20 2C 31 41 3D  ..".. ....r ,1A=
0060: 36 01 95 78 23 C2 46 56   D1 9E DC E4 22 E9 0E A9  6..x#.FV...."...
0070: 99 2E 27 EC 96 D4 41 F2   C0 7A 89 2D 02 AF FB F5  ..'...A..z.-....

]

Added certificate to keystore 'jssecacerts' using alias 'FR-MCRAIG-01-1'

In InstallCert.java the default passphrase is changeit. In addition to the change of port number and the use of ldaps:// instead of ldap://, I added this Java to the top of the conf.jsp in the application posted.

// Use the keystore that contains the OpenDS cert
String keystorePath = System.getProperty("java.home") +
        "\\\\lib\\\\security\\\\jssecacerts";
System.setProperty("javax.net.ssl.keyStore", keystorePath);
System.setProperty("javax.net.ssl.keyStorePassword", "changeit");

Hope it helps.

Python LDAP StartTLS to OpenDS

Download OpenDSThe OpenDS server on my laptop is using a self-signed cert. The trick to using StartTLS -- to avoid sending passwords over the network in clear text -- seems to be telling the underlying software to consider this self-signed cert as a CA cert.

Perhaps not safe for production, but seems to work for testing.

  1. Configure OpenDS to permit Start TLS.
    Easiest way to do this is at install time with Quick Setup.
  2. Bring up the OpenDS Control Panel.
    1. In the Control Panel Manage Entries window select Base DN: All Base DNs.
    2. Under cn=admin data > instance keys > ds-cfg-instance-key has a ds-cfg-public-key-certificate;binary attribute value, visible when you click Edit...
    3. Copy that server cert value.
  3. Save the server cert to a .pem file.
    Adding the ...BEGIN... and ...END... lines and playing with the formatting gave me this.
    C:\\>more opends-cert.pem
    -----BEGIN CERTIFICATE-----
    MIIB3zCCAUigAwIBAgIESng5szANBgkqhkiG9w0BAQUFADA0MRswGQYDVQQKExJP
    cGVuRFMgQ2VydGlmaWNhdGUxFTATBgNVBAMTDEZSLU1DUkFJRy0wMTAeFw0wOTA4
    MDQxMzM3NTVaFw0yOTA3MzAxMzM3NTVaMDQxGzAZBgNVBAoTEk9wZW5EUyBDZXJ0
    aWZpY2F0ZTEVMBMGA1UEAxMMRlItTUNSQUlHLTAxMIGfMA0GCSqGSIb3DQEBAQUA
    A4GNADCBiQKBgQCNnnsxIx7dBdx79Ny7b9uptn+db6eu8qHoGDfaTBFOoEU+Sl7f
    AW9g3ArSD67kKkmTZnZl/uonSM7+1Mni32/7HyrEQvkZDr1DfndUDG8eVkaP1u/D
    XcZNPpEGizchFR+vXbCcA45KZFr54/JakdUAABxlevlfrhlo5N5sQH8HIQIDAQAB
    MA0GCSqGSIb3DQEBBQUAA4GBAErAmCUBeBvKCaL2wPeulPz0HoyumFfdC21LAid7
    x2tq7EuniCBEiz1mt04sIRYB2iBKPOQ5uIcQXEpo4zllM1yIeCXretWFzr7EhzsF
    JlyZRJIaOe5IdkTm9XHENeLACveYd25QsUVClDTPVZHe0AOsH6X2xHQHMCIwSwuw
    9pl4
    -----END CERTIFICATE-----
  4. Tell the Python LDAP code to pretend the file contains a CA cert.
    Here, make sure that the hostname you use is the same as in the cert.
    >>> import ldap
    >>> ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
    >>> ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,"C:\\opends-cert.pem")
    >>> l = ldap.initialize("ldap://FR-MCRAIG-01:1389")
    >>> l.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
    >>> l.set_option(ldap.OPT_X_TLS,ldap.OPT_X_TLS_DEMAND)
    >>> l.start_tls_s()
    >>> l.simple_bind_s('uid=kvaughan,ou=people,dc=example,dc=com','bribery')
    (97, [])
    >>> l.whoami_s()
    'dn:uid=kvaughan,ou=People,dc=example,dc=com'
    >>>

If you cannot figure out what hostname is in the self-signed server cert, have a look with ldapsearch.

D:\\SunOpenDS_SE2.0\\bat>ldapsearch --useStartTLS -p 1389 -b dc=example,dc=com uid=bjensen
The server is using the following certificate:
    Subject DN:  CN=FR-MCRAIG-01, O=OpenDS Self-Signed Certificate
    Issuer DN:  CN=FR-MCRAIG-01, O=OpenDS Self-Signed Certificate
    Validity:  Tue Aug 04 15:35:44 CEST 2009 through Thu Aug 04 15:35:44 CEST 2011
Do you wish to trust this certificate and continue connecting to the server?
Please enter "yes" or "no":yes
dn: uid=bjensen,ou=People,dc=example,dc=com
...

LDAP Client Best Practices

In 2006 Ludo helped me compose a list of LDAP best practices for a developer guide I was writing for Sun. This chapter of the guide was removed before publication.

When Developing Specify LDAP v3

Many client libraries default to LDAP v2, but you can elect to use LDAP v3. To benefit from LDAP v3 features, you can set up the connection, and then authenticate explicitly using LDAP v3.

  • With JNDI, you could use LDAP v3 as shown here.

    import java.util.Hashtable;
    import javax.naming.ldap.InitialLdapContext;
    
    Hashtable env = new Hashtable();
    env.put("java.naming.ldap.version", "3");
    InitialLdapContext ctx = new InitialLdapContext(env, null);

  • With the Mozilla LDAP C SDK, you could use LDAP v3 as shown here.

    #include "ldap.h"
    
    int version = LDAP_VERSION3;
    ldap_set_option( NULL, LDAP_OPT_PROTOCOL_VERSION, &version );

    Mozilla LDAP C SDK uses LDAP v3 by default.

  • With the Mozilla LDAP Java SDK, you could use LDAP v3 as shown here.

    import netscape.ldap.LDAPConnection;
    
    LDAPConnection ld = new LDAPConnection();
    ld.setOption(LDAPv3.PROTOCOL_VERSION, new Integer(3));

When Developing Authenticate Correctly

In LDAP v3, you connect, then you bind and perform LDAP operations, then you unbind and disconnect. The bind is the authentication operation in LDAP. Your application can hold onto a connection but change the authentication credentials by using the bind operation again.

Some directories do not allow anonymous access, even for reads. When you build your application, keep the option that allows users to authenticate to the directory. Furthermore, the information sent across the network can be sensitive. You can protect sensitive data by allowing the application to secure the connection by using Secure Sockets Layer (SSL) or Start Transport Layer Security (TLS).

If your application needs to authenticate, obtain a regular account to authenticate with the directory, rather than using the directory superuser account (such as cn=Directory Manager). When you authenticate as directory superuser, you bypass normal access control mechanisms. Bypassing normal access control renders auditing directory access more difficult.

When authenticating, have your application use SSL or SASL DIGEST MD5 to avoid sending passwords over the network in clear text. Furthermore, when using password-based authentication, have your application check password policy controls, especially to determine when a password must be renewed.

When Developing Limit Connection Overhead

A new connection requires system resources. The LDAP model allows you to reuse connections by binding again with a different identity on the same connection. Thus, you can avoid the costs of new connections, particularly negotiated connections such as connections that use SSL, by reusing connections. Your application can use a pool of connections, rebinding when necessary. Your application can alternatively use the proxy authorization control to remain authenticated as the application but perform operations on behalf of a particular user.

When establishing a connection, your application can provide alternate server host names and port numbers to facilitate failover that is transparent to the application. You can also set time limits for LDAP operations to avoid getting blocked.

When finished with a connection, your application should perform an unbind.

When Developing Handle Potential Inactivity Timeouts

Most network equipment can use timeouts to drop stale connections, ensuring the equipment keeps a maximum number of connections that are available.

If your application pools connections or opens connections for persistent search, than guard against timeouts that drop those connections. Use the connections occasionally to reset inactivity timers present in the network.

Alternatively, if you have control over the connection, consider disabling inactivity time outs for your applications that need to keep persistent connections open. Load balancers and proxy software often use inactivity timeouts.

When Developing Retrieve Entries Intelligently

LDAP servers typically respond quickly to requests for entries. Yet, the server can respond most quickly when your application asks it to do only necessary work. If you need to read only a few attributes in an entry, request each attribute explicitly. Avoid reading the entire entry, then parsing the entire entry to obtain the required data.

Furthermore, when you do request attributes in an entry, retrieve all the required attributes at once. Each new request involves a new operation on the server.

If any of the attributes that you require are operational attributes, you must request those attributes specifically. With Sun Directory Server, operational attributes are identifiable in the directory schema by their USAGE, which is directoryOperation or dsaOperation.

When retrieving entries and attributes, recognize that you might not have access to all the attributes that exist.

When Developing Write Simple, Conforming LDAP Filters

The best filters use attributes that are indexed according to the way the attributes are indexed. For example, if employeeNumber is indexed for equality, your filter should be an equality filter such as (employeeNumber=12345). Do not use a substring filter instead.

Avoid deeply nested complex filters when you can. When you must use complex filters, place the most specific filters first to narrow the list of candidate entries the directory must check. For best results, use not, !, only with and, &, for example (&(cn=Barbara)(!(sn=Jensen))). When you use not with or in a filter, the directory must construct a candidate list of everything except what your filter specifies.

When Developing Make Modifications Specific

Modifications are atomic on the entry to which the modifications apply. When modifying multivalued attributes, delete and replace specific values. Do not replace an entire list of multiple values to change only a few values. Replacing specific values is particularly good practice when the changes must be replicated across a set of servers.

Moreover, when you have very large values to store in an attribute, store a reference to the data instead of storing the data object.

When Developing Trust Result Codes

LDAP replication trades tight consistency across replica servers for very high performance, availability, and scalability. By allowing loose consistency of data across sets of replica servers, individual servers can respond very quickly to your application. Yet, data replication is not instantaneous. A short but detectable delay can ensue after a server returns success for a write operation, but before the effects are seen on other replicas.

Therefore, when your application receives a result code from an LDAP to indicate that an operation was successful, your application should trust the result code. When application requests are balanced across replicas, reading from another replica might result in errors due to a slight delay in replication.

Directory administrators can get around applications that do not trust result codes for example by configuring Sun Directory Proxy Server to apply server affinity, which routes operations from the same client to the same server.

When Developing Limit Dealings With Groups and Roles

When you want to know whether an account belongs to a group or a role, read only the necessary attribute values. Do not read the entire list of group members.

For static groups, Sun Directory Server and OpenDS offer the isMemberOf attribute on the user entry. With Sun Directory Server 5.x versions, compare the DN of the account to the uniqueMember attribute of the group, such as (uniquemember=uid=bjensen,ou=people,dc=example,dc=com). Or use a filter to find all the static groups to which a user belongs, such as (&(member=uid=bjensen,ou=people,dc=example,dc=com)(objectclass=groupofnames)).

For dynamic groups, do the following:

  1. Read the URL from the group definition.

  2. Examine the host, DN, and scope of the URL.

  3. Apply the filter part of the URL to the entry for the account.

For Sun Directory Server roles, compare the DN of the role to the nsRole attribute of the entry for the account, such as (nsrole=cn=management,ou=people,dc=example,dc=com).

When Developing Read the DSE

The root DSE is the entry that is retrieved by ldapsearch -b "" -s base "(objectclass=\*)". The root DSE describes server capabilities. The root DSE contains information about supported LDAP protocol versions, naming contexts (suffixes), LDAPv3 controls, LDAPv3 extensions, and authentication mechanisms. The root DSE can contain information about the server version.

Some directory administrators protect access to the root DSE. Yet, applications might read the root DSE to confirm that the server in fact supports functionality required by applications.

When Developing Use Resource-Intensive Features Sparingly

Directories offer powerful features that can nevertheless place a heavy load on the server. Two such features are persistent search, and server-side sorting.

Persistent search lets you start a search that does not stop when complete, but instead allows you to receive updates when entries are modified. To provide this feature, the server must handle your search when anything happens to an entry in its scope.

Server-side sorting requires that the server sort the entries that are returned during a search. Instead of returning entries as quickly as possible, the server must therefore get the list to return, and sort the list.

When Developing Avoid Hard Coding Information About Data

The container entry for a subtree might be not be identical on different directories. Rather than hard code the container entry throughout your application, locate the container entry. Then navigate beneath the container entry in the tree.

Object classes and attribute types for the same information can also differ from directory to directory. Use configuration files, properties files, or other easily modifiable variables rather than hard coding object class and attribute type identifiers into your application.

Be aware as well that object class and attribute type identifiers are not case-sensitive in LDAP. Your application should therefore recognize that inetOrgPerson and inetorgperson are equivalent, as are isMemberOf and ismemberof.

When Developing Define Schemas Only When Necessary

Schemas define the object classes and attribute types that are recognized by the directory. If your application can use a standard schema, use the standard schema. LDAP server schemas typicall define numerous standard object classes, and attribute types.

When you must define your own schema objects, follow these guidelines:

  • Extend existing object classes by using AUXILIARY classes.

  • Create new attributes rather than redefining existing attributes.

    Other applications might depend on existing attributes to keep their existing semantics.

  • Obtain new object identifiers for the schema elements you define, rather than reusing existing object identifiers.

  • Obtain new names for the schema elements you define, rather than reusing existing names.

  • Update schema over LDAP if you can.

When Developing Handle Referrals

LDAPv3 allows directories that are unable to handle your request to refer your application to other directories. Your application should follow those referrals.

When following referrals, realize that authentication procedures might not be exactly the same on different directories. Also, directories that refer to each other could potentially cause a referral loop. With the Mozilla Directory SDKs, you can limit referral hops to prevent your application from being referred endlessly from one directory to another directory. The JNDI interface enables you to follow referrals automatically.

When Developing Treat a Directory as a Directory

A directory is typically a repository for identity data, and for information that you expect to keep for awhile and read often. You might typically find relational databases better adapted to hold transient data such as session keys and presence information, or voluminous accumulated data such as application logs.

When Troubleshooting Check Result Codes

When an LDAP request from your application fails on the server, the server sends back a result code, and possibly an explanatory message. Your application should check the result codes, and for explanatory messages. Common failure result codes include the following, which are expressed as decimal values. Others result codes are defined as well.

1

LDAP operations error. The server encountered an error while processing your request.

32

No such object. The entry is not present on the server. Also, no referral is defined for the entry.

49

Invalid credentials. Your application failed to authenticate properly.

53

LDAP unwilling to perform. The directory does not support the request. Alternatively, the directory is not currently in a state in which to complete your request. For example, the directory might be in read-only mode when your application requests a modification.

65

Object class violation. Your write request would cause an entry to no longer conform to the schema defined for the directory.

68

Already exists. Your application is requesting to add an entry that has the same DN as an entry already present in the directory.

RFC 4511 defines LDAP error codes.

When Troubleshooting Check Server Log Files

OpenDS and Sun Directory Server log messages related to server operation in its instance-path/logs/errors file. If you have access to this file, you might find useful troubleshooting information there. When debugging your application against Sun Directory Server, you can adjust the log level using the dsconf set-log-prop error level:your-setting command. Log level settings are explained in the log(5dsconf) man page. If you use OpenDS, see https://www.opends.org/wiki/page/HowToConfigureLogsWithDsconfig.

When Troubleshooting Inspect Network Packets

Although LDAP is not a textual protocol, tools such as snoop(1M), ethereal, and tcpdump can decode the packets, sometimes providing you with important debugging information. SLAMD also provides an LDAP decoder to display LDAP packets in human-readable format.

(How many of those rules did I break in Java, PHP, and Python? ;-)

Thursday Aug 20, 2009

LDAP Basics With PHP

PHP is another web application language you know better than I do, but that does not make it hard to use PHP to access a directory.

After installing PHP with the wrappers for the OpenLDAP libraries, I replicated the small LDAP web application posted earlier using Java with JNDI. (PHP version zip download) I had already installed OpenDS SE 2.0, and imported Example.ldif for the data.

For the user, the only changes are the colors and the title.

ready to authenticate

The user kvaughan is present in Example.ldif.

auth success

Here is the PHP code for that:

        <h2>LDAP Authentication Results</h2>
        <p>Return to <a href="index.php">top page</a>.</p><hr />
        <?php
        include 'conf.php';

        $user = htmlspecialchars($_POST['user']);
        $filter = "(|(uid=" . $user . ")" . "(mail=" . $user ."@\*))";

        echo "<p>Equivalent command line:<br /><tt>ldapsearch -h " .
        $server . " -p " . $port . " -b " . $basedn . " \\"" .
        $filter . "\\"</tt></p>";
        echo "<hr />";

        // Connect to the LDAP server.
        $ldapconn = ldap_connect($server, $port) or
        die("Could not connect to " . $server . ":" . $port . ".");

        // Bind anonymously to the LDAP server to search and retrieve DN.
        $ldapbind = ldap_bind($ldapconn) or die("Could not bind anonymously.");
        $result = ldap_search($ldapconn,$basedn,$filter) or die ("Search error.");
        $entries = ldap_get_entries($ldapconn, $result);
        $binddn = $entries[0]["dn"];
        echo "<p>Bind DN found: ". $binddn . "</p>";
        echo "<hr />";

        // Bind again using the DN retrieved. If this bind is successful,
        // then the user has managed to authenticate.
        $ldapbind = ldap_bind($ldapconn, $binddn, $_POST['password']);
        if ($ldapbind) {
            echo "Successful authentication for " . $user . ".";
        } else {
            echo "Failed authentication for " . $user . ".";
        }

        ldap_close($ldapconn);
        ?>
        <hr /><p>Return to <a href="index.php">top page</a>.</p>

Again, searching is even easier.

ready to search

...search results coming up...

search success

Here is the PHP code for the search.

        <h2>LDAP Search Results</h2>
        <p>Return to <a href="index.php">top page</a>.</p><hr />
        <?php
        include 'conf.php';

        // Thanks to http://www.devshed.com/c/a/PHP/Using-PHP-With-LDAP-part-1
        // for inspiration.
        $name = htmlspecialchars($_POST['name']);
        $filter = "(|(cn=\*" . $name . "\*)" . "(sn=\*" . $name ."\*))";

        echo "<p>Equivalent command line:<br /><tt>ldapsearch -h " .
        $server . " -p " . $port . " -b " . $basedn . " \\"" .
        $filter . "\\"</tt></p>";
        echo "<hr />";

        // Connect to the LDAP server.
        $ldapconn = ldap_connect($server, $port) or
        die("Could not connect " . $server . ":" . $port . ".");

        // Bind anonymously to the LDAP server to search.
        $ldapbind = ldap_bind($ldapconn) or die("Could not bind anonymously.");
        $result = ldap_search($ldapconn,$basedn,$filter) or die ("Search error.");
        $entries = ldap_get_entries($ldapconn, $result);

        // Display key data for each entry.
        for ($i=0; $i<$entries["count"]; $i++) {
            echo "<p>DN: " . $entries[$i]["dn"] . "<br />";
            echo "Uid: " . $entries[$i]["uid"][0] . "<br />";
            echo "Email: " . $entries[$i]["mail"][0] . "</p>";
        }

        ldap_close($ldapconn);
        ?>
        <hr /><p>Return to <a href="index.php">top page</a>.</p>

Wednesday Aug 19, 2009

OpenDS and SMF

Download OpenDSFor folks using OpenDS on Solaris and OpenSolaris, Brad Diggs offers a blog entry with an example script for simplifying OpenDS SMF management.

The service management facility provides the framework to manage one or more OpenDS instances as a service, so you can have OpenDS start at boot time, have instances restarted automatically, and so forth.

Monday Aug 17, 2009

LDAP Basics With Java

You can access LDAP through Python, but what about Java applications? As mentioned earlier, JNDI technology supports directory access from Java applications, and is part of the Java platform.

You are probably writing a web-facing application that sits in an application server like GlassFish. I wrote a quick a dirty web application (⁞zipped here) that does authentication and searches in much the same way as the Python code. I had already installed OpenDS SE 2.0, and imported Example.ldif for the data. Here is the top page.

ready to authenticate

Using the hint, I authenticated as bjensen.

successful auth

Here's the relevant JSP code.

        <h2>LDAP Authentication Results</h2>
        <p>Return to <a href="index.jsp">top page</a>.</p><hr />
        <%
        String user = request.getParameter("user");
        String password = request.getParameter("password");

        String filter = "(|(uid=" + user + ")" + "(mail=" + user + "@\*))";
        String cliEquiv = "<tt>ldapsearch -h " + server + " -p " +
                port + " -b " + basedn + " \\"" + filter + "\\"</tt></p>";
        %>
        <p>Equivalent command line:<br /><%= cliEquiv%><hr />
        <%
        // Connect to the LDAP server.
        Hashtable env = new Hashtable(11);
        env.put(Context.INITIAL_CONTEXT_FACTORY,
                "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, "ldap://" + server + ":" + port + "/");

        // Search and retrieve DN.
        try {
            LdapContext ldap = new InitialLdapContext(env, null);
            NamingEnumeration results = ldap.search(basedn, filter, null);
            String binddn = "None";
            while (results.hasMore()) {
                SearchResult sr = (SearchResult) results.next();
                binddn = sr.getName() + "," + basedn;
            }
        %>
        <p>Bind DN found: <%= binddn%><hr /></p>
        <%
            ldap.close();

            // Authenticate
            env.put(Context.SECURITY_AUTHENTICATION, "simple");
            env.put(Context.SECURITY_PRINCIPAL, binddn);
            env.put(Context.SECURITY_CREDENTIALS, password);

            ldap = new InitialLdapContext(env, null);
        %>
        <p>Successful authentication for <%= user%>.</p>
        <%
        } catch (AuthenticationException ae) {
            ae.printStackTrace();
        %>
        <p>Failed authentication for <%= user%>.</p>
        <%
        } catch (NamingException e) {
            e.printStackTrace();
        }
        %>
        <hr /><p>Return to <a href="index.jsp">top page</a>.</p>

Searches are even simpler.

ready to search

This returns a page with all the users having jensen in their name.

search success

The relevant JSP code for this one follows.

        <h2>LDAP Search Results</h2>
        <p>Return to <a href="index.jsp">top page</a>.</p><hr />
        <%
        String name = request.getParameter("name");

        String filter = "(|(cn=\*" + name + "\*)" + "(sn=\*" + name + "\*))";
        String cliEquiv = "<tt>ldapsearch -h " + server + " -p " +
                port + " -b " + basedn + " \\"" + filter + "\\"</tt></p>";
        %>
        <p>Equivalent command line:<br /><%= cliEquiv%><hr />
        <%
        // Connect to the LDAP server.
        Hashtable env = new Hashtable(11);
        env.put(Context.INITIAL_CONTEXT_FACTORY,
                "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, "ldap://" + server + ":" + port + "/");

        // Search for entries and display results.
        try {
            LdapContext ldap = new InitialLdapContext(env, null);
            NamingEnumeration results = ldap.search(basedn, filter, null);
        %>
        <pre>
        <%
            String noresult = "";
            if (!results.hasMoreElements()) noresult = "No results found.";
        %>
<%= noresult %><%
            while (results.hasMore()) {
                SearchResult sr = (SearchResult) results.next();
                Attributes attrs = sr.getAttributes();
                Attribute uid = attrs.get("uid");
                Attribute mail = attrs.get("uid");
        %>
DN   : <%= sr.getName() + "," + basedn%>
Uid  : <%= uid%>
Email: <%= mail%>
        <%
            }
            ldap.close();
        } catch (NamingException e) {
            e.printStackTrace();
        }
        %>
        </pre>

Thursday Aug 13, 2009

LDAP Basics With Python

In an earlier entry I mentioned the Python LDAP API. Python is not a familiar language, but does get easier each time I relearn it.

After finding the modules through http://www.python-ldap.org/ and getting everything installed on a Windows XP laptop, I installed ⁞OpenDS SE 2.0, importing Example.ldif to play with some familiar data.

What follows is a Python session in IDLE, minus typos, showing quick and dirty authentication and searching. ("Quick and dirty" because you would never send a user password over the network in clear text. Maybe some more on that one later.)

Python 2.5.2 (r252:60911, Feb 21 2008, 13:11:45) [MSC v.1310 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.

    \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
    Personal firewall software may warn about the connection IDLE
    makes to its subprocess using this computer's internal loopback
    interface.  This connection is not visible on any external
    interface and no data is sent to or received from the Internet.
    \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
    
IDLE 1.2.2      
>>> # Download LDAP modules through http://www.python-ldap.org/
>>> import ldap,ldif,sys
>>> 
>>> ### Authenticate
>>> # Get a connection to your LDAP server
>>> ld = ldap.initialize('ldap://localhost:1389')
>>>
>>> # Authenticate based on user id/email and password
>>> user = "bjensen"
>>> password = "hifalutin"
>>> 
>>> # Bind anonymously to search for the user entry DN
>>> ld.simple_bind_s()
(97, [])
>>>
>>> basedn = "ou=people,dc=example,dc=com"
>>> filter = "(|(uid=" + user + "\*)(mail=" + user + "\*))"
>>> results = ld.search_s(basedn,ldap.SCOPE_SUBTREE,filter)
>>> for dn,entry in results:
	dn = str(dn)

	
>>> # Bind with the DN and password
>>> ld.simple_bind_s(dn,password)
(97, [])
>>> # Bind was successful
>>> ld.whoami_s()
'dn:uid=bjensen,ou=People,dc=example,dc=com'
>>> 
>>> 
>>> ### Search
>>> 
>>> ld = ldap.initialize('ldap://localhost:1389')
>>> 
>>> # Lookup entries with Common Name or Surname containing 
>>> name = "jensen"
>>> 
>>> ld.simple_bind_s()
(97, [])
>>> basedn = "ou=people,dc=example,dc=com"
>>> filter = "(|(cn=\*" + name + "\*)(sn=\*" + name + "\*))"
>>> results = ld.search_s(basedn,ldap.SCOPE_SUBTREE,filter)
>>>
>>> ldif_writer = ldif.LDIFWriter(sys.stdout)
>>> for dn,entry in results:
	ldif_writer.unparse(dn,entry)

	
dn: uid=kjensen,ou=People,dc=example,dc=com
cn: Kurt Jensen
facsimileTelephoneNumber: +1 408 555 8721
givenName: Kurt
l: Santa Clara
mail: kjensen@example.com
objectClass: person
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: top
ou: Product Development
ou: People
roomNumber: 1944
sn: Jensen
telephoneNumber: +1 408 555 6127
uid: kjensen

dn: uid=bjensen,ou=People,dc=example,dc=com
cn: Barbara Jensen
cn: Babs Jensen
facsimileTelephoneNumber: +1 408 555 1992
givenName: Barbara
l: Cupertino
mail: bjensen@example.com
objectClass: person
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: top
ou: Product Development
ou: People
roomNumber: 0209
sn: Jensen
telephoneNumber: +1 408 555 1862
uid: bjensen

dn: uid=gjensen,ou=People,dc=example,dc=com
cn: Gern Jensen
facsimileTelephoneNumber: +1 408 555 9751
givenName: Gern
l: Santa Clara
mail: gjensen@example.com
objectClass: person
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: top
ou: Human Resources
ou: People
roomNumber: 4609
sn: Jensen
telephoneNumber: +1 408 555 3299
uid: gjensen

dn: uid=jjensen,ou=People,dc=example,dc=com
cn: Jody Jensen
facsimileTelephoneNumber: +1 408 555 8721
givenName: Jody
l: Sunnyvale
mail: jjensen@example.com
objectClass: person
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: top
ou: Accounting
ou: People
roomNumber: 4882
sn: Jensen
telephoneNumber: +1 408 555 7587
uid: jjensen

dn: uid=ajensen,ou=People,dc=example,dc=com
cn: Allison Jensen
facsimileTelephoneNumber: +1 408 555 0111
givenName: Allison
l: Santa Clara
mail: ajensen@example.com
objectClass: person
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: top
ou: Product Development
ou: People
roomNumber: 0784
sn: Jensen
telephoneNumber: +1 408 555 7892
uid: ajensen

dn: uid=bjense2,ou=People,dc=example,dc=com
cn: Bjorn Jensen
facsimileTelephoneNumber: +1 408 555 4774
givenName: Bjorn
l: Santa Clara
mail: bjense2@example.com
objectClass: person
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: top
ou: Accounting
ou: People
roomNumber: 4294
sn: Jensen
telephoneNumber: +1 408 555 5655
uid: bjense2

dn: uid=tjensen,ou=People,dc=example,dc=com
cn: Ted Jensen
facsimileTelephoneNumber: +1 408 555 3825
givenName: Ted
l: Santa Clara
mail: tjensen@example.com
objectClass: person
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: top
ou: Accounting
ou: People
roomNumber: 4717
sn: Jensen
telephoneNumber: +1 408 555 8622
uid: tjensen

dn: uid=rjensen,ou=People,dc=example,dc=com
cn: Richard Jensen
facsimileTelephoneNumber: +1 408 555 3825
givenName: Richard
l: Cupertino
mail: rjensen@example.com
objectClass: person
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: top
ou: Accounting
ou: People
roomNumber: 2631
sn: Jensen
telephoneNumber: +1 408 555 5957
uid: rjensen

dn: uid=rjense2,ou=People,dc=example,dc=com
cn: Randy Jensen
facsimileTelephoneNumber: +1 408 555 1992
givenName: Randy
l: Sunnyvale
mail: rjense2@example.com
objectClass: person
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: top
ou: Product Testing
ou: People
roomNumber: 1984
sn: Jensen
telephoneNumber: +1 408 555 9045
uid: rjense2

>>> ld.unbind_s()
>>>

Tuesday Aug 11, 2009

LDAP Client APIs

In the LDAP client-server model, directory servers make information about people, organizations, and resources accessible to LDAP client applications. LDAP defines operations that clients use to search and update the directory. An LDAP client can perform these operations, among others:

  • Search for and retrieve entries from the directory
  • Add new entries to the directory
  • Update entries in the directory
  • Delete entries from the directory
  • Rename entries in the directory

For example, to update an entry, an LDAP client submits the distinguished name (DN) of the entry with updated attribute information to the LDAP server. The LDAP server uses the distinguished name to find the entry. The server then performs a modify operation to update the entry in the directory.

To perform any of these LDAP operations, an LDAP client needs to establish a connection with an LDAP server. The LDAP protocol specifies the use of TCP/IP port number 389, although servers can listen on other ports, such as 636 for LDAP/SSL for example.

The LDAP protocol also defines a simple method for authentication. LDAP servers can be set up to restrict permissions to the directory. Before an LDAP client can perform an operation on an LDAP server, the client must authenticate to the server. Clients typically authenticate by supplying a distinguished name and password. If the user identified by the distinguished name does not have permission to perform the operation, the server does not execute the operation.

On the web you can find free LDAP directory client software development kits for creating your own LDAP clients.

Java

  • Java Naming and Directory Interface (JNDI) technology supports directory access through LDAP and DSML from Java applications, and is part of the Java platform. With JNDI, you can build powerful, portable, directory-enabled applications that do not depend on classes outside the Java platform. The upside is that you do not need to install additional client libraries. JNDI provides an abstract model that lets you access not only directories, but also naming services in general, including DNS, RMI, COS, and file systems. The downside is that even if you already know LDAP, you still have to learn JNDI. The JNDI Tutorial contains descriptions and examples of how to use JNDI. The tutorial is at http://java.sun.com/products/jndi/tutorial/.
  • The Mozilla LDAP Java SDK offers an API that is more readily comprehensible than JNDI if you already know LDAP. The API was the subject of Internet-Draft work that did not result in a standard. The code is published in open source form as part of the Mozilla Directory SDK project. See http://www.mozilla.org/directory/.
  • LDAP SDK for Java from UnboundID provides a recent Java implementation of an LDAP API, with additional features not available in older Java APIs. For more, see http://www.unboundid.com/products/ldapsdk/.
  • LDAP Classes for Java follow the Internet-Draft work. Novell makes the LDAP Classes for Java available through their developer community. See http://developer.novell.com/wiki/index.php/LDAP_Classes_for_Java.

C

  • The Mozilla LDAP C SDK, also based on the Internet-Draft work that did not become a standard, is available on a wide range of platforms. The Mozilla LDAP C SDK also provides support for core LDAP operations, and for LDAP v3 extensions and widely used controls. Sun Directory Server uses this API, as does the address book applications associated with Firefox. (Try a valid LDAP URL in your browser.)  Mozilla LDAP C SDK code is published in open source form as part of the Mozilla Directory SDK project. See http://www.mozilla.org/directory/.
  • OpenLDAP C API  Many GNU/Linux distributions provide OpenLDAP support. The OpenLDAP C API is based on an Internet-Draft for that never became a standard. The API closely follows LDAPv3, providing support for core LDAP operations and for LDAPv3 extensions and widely used controls. LDAP support for languages such as PHP and Python is available through wrappers for OpenLDAP. For an introduction to the OpenLDAP API, see the LDAP(3) man page.
  • Solaris LDAP C API  The native LDAP library on Solaris systems provides nearly the same API as the Mozilla C SDK. Many clients need only be recompiled to work with Solaris libldap. The LDAP library on Solaris systems is not however compatible with libldap from OpenLDAP. For an introduction to the Solaris OS libldap library, see the ldap(3LDAP) man page. (Thanks for reading my non-cross-platform plug. ;-)

Perl

PHP

PHP can be compiled with LDAP wrappers for the OpenLDAP C implementation. You must download the OpenLDAP libraries to use the PHP LDAP API. See http://php.net/manual/en/book.ldap.php.

Python

The python-ldap package wraps the OpenLDAP C implementation, with additional capabilities to handle LDIF, LDAP URLs, and so forth. See http://www.python-ldap.org/.

Other Languages

The list here is a start. A number of other languages also provide LDAP support. Ruby has the Ruby/LDAP extension module. An Objective-C LDAP framework is available. So is LDAP support in C#. There are no doubt others.

Wednesday Jul 29, 2009

OpenDS interview at Javalobby

Download OpenDSGeertjan Wielenga interviewed Ludo about OpenDS for Javalobby, see http://java.dzone.com/news/opends-open-source-directory.

OpenDS has been open source now for 3 years. Ludo and Geertjan cover some of the capabilities of the LDAPv3 compliant directory service now that 2.0 is out, as well as where OpenDS came from, and where it's going.

Monday Jul 27, 2009

DirTracer Updates

Download DSEE free of chargeLee Trujillo has published a webcast on DirTracer, a great tool for gathering info around Directory support. Also, as Lee mentions in his blog entry on the subject, DT 6.0.7 and Pkgapp 3.2 (Linux) Available! this update supports Solaris (SPARC and x86), Red Hat, and SuSE.

Lee lists new features and enhancements in http://blogs.sun.com/Dirtracer/entry/dt_6_0_7_coming. Folks outside Sun's network can get DirTracer though Sun support until it becomes available through BigAdmin.

About

Mark Craig writes about Directory Services products and technologies. The views expressed on this blog are my own and do not necessarily reflect the views of Oracle.

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today