Soft Deletes in the Ping Identity Directory Server

As its name implies, the LDAP delete operation removes an entry from the directory server. Typically, this completely removes the entry from the server, but there may be times when you would prefer for the entry to be hidden from LDAP clients, while still available in the server for at least a period of time.

The Ping Identity Directory Server offers this capability in the form of soft deletes. A soft-deleted entry still exists in the server, but it is renamed so that the DN includes the entry’s entryUUID value, and a special ds-soft-delete-entry object class is added that ensures the entry won’t be visible to most clients, and to provide additional metadata about the soft delete operation (including the entry’s original DN, the time the entry was soft-deleted, and the authorization DN and IP address of the client that requested it).

Using soft deletes can offer a number of benefits. Some of them may include:

  • It makes it easier to resurrect an entry if it is removed in error. We also provide a simple way to undelete a soft-deleted entry to restore it to “regular entry” status.
  • It provides LDAP-accessible auditing information about the delete operation. Even though soft-deleted entries aren’t visible to most clients, we do provide ways for authorized clients to see them if they’re specifically looking for them.
  • You can use this to prevent reuse of values, even after an entry has been deleted. For example, say that you’re an email provider and you don’t ever want to allow an email address to be reused, even if the former owner has removed their account. The unique attribute plugin has support for either permitting or rejecting conflicts with soft-deleted entries.

There are two ways that you can perform soft deletes in the Ping Identity Directory Server: you can configure the server to automatically turn regular deletes matching a given set of criteria into soft deletes, or you can explicitly request them with the soft delete request control. But before you can do either one, you need to set a soft delete policy.

Configuring the Server’s Soft Delete Policy

A soft delete policy can be used to specify the conditions under which the server should automatically turn regular delete operations into soft deletes, and can also be used to indicate the conditions under which the server should automatically clean up soft-deleted entries.

There are two properties that can be used to specify the conditions under which the server should automatically turn regular deletes into soft deletes:

  • auto-soft-delete-connection-criteria — A reference to a connection criteria object that specifies the clients whose delete requests should automatically be turned into soft deletes. This criteria can include anything the server knows about the requester, including their identity (where their entry is in the DIT, the contents of their entry, their group memberships, etc.), the address of the client, whether the communication is secure, and the protocol they are using to communicate with the server.
  • auto-soft-delete-request-criteria — A reference to a request criteria object that specifies which delete requests should automatically be turned into soft deletes. This criteria can include anything the server knows about the delete request, including the location of the target entry in the DIT, the content of that entry, the groups in which that entry is a member, the controls included in the request, and the origin of the request (e.g., directly requested by a client, replicated from another server, initiated by a component within the server, etc.).

If neither of these properties has a value, then only delete requests that include the soft delete request control will be treated as soft deletes. If only one of them has a value, then all delete requests that match that criteria object (or that include the soft delete request control) will be treated as soft deletes. If both of them have values, then only delete requests that match both sets of criteria (or that include the soft delete request control) will be treated as soft deletes.

By default, soft-deleted entries will remain in the server forever (or until someone explicitly deletes them), but you can also configure the server to automatically delete them under certain conditions. The soft delete policy offers two properties that can be used to control this:

  • soft-delete-retention-time — The maximum length of time that soft-deleted entries should be retained in the server before they are eligible to be automatically removed.
  • soft-delete-retain-number-of-entries — The maximum number of soft-deleted entries that should be retained in the server.

If either or both of these properties is configured, then soft-deleted entries that fall outside of either one of them will be eligible for removal. If neither is configured, then soft-deleted entries won’t be automatically removed by the server.

To enable soft delete functionality in the server, you need to create a soft delete policy, and you also need to update the global configuration to make it the active policy. With no active soft delete policy, the server will not automatically turn any deletes into soft deletes, nor will it allow clients to use the soft-delete request control.

Example 1: Only Explicit Soft Deletes Without Automatic Cleanup

If you don’t want the server automatically turning regular deletes into soft deletes, but you do want to allow clients to use the soft delete request control, and if you don’t want the server to automatically clean up any soft-deleted entries, then you can just create a soft delete policy with the default settings and make that the active policy. You can do that with the following configuration changes:

dsconfig create-soft-delete-policy \
     --policy-name "Explicit Soft Delete Requests Without Cleanup"

dsconfig set-global-configuration-prop \
     --set "soft-delete-policy:Explicit Soft Delete Requests Without Cleanup"

Example 2: Automatic Soft Deletes With Automatic Cleanup

If you want the server to automatically turn all delete operations into soft deletes, and to keep soft-deleted entries around for 30 days, you can do that with the following changes:

dsconfig create-request-criteria \
     --criteria-name "All Delete Requests" \
     --type simple \
     --set operation-type:delete

dsconfig create-soft-delete-policy \
     --policy-name "Automatic Soft Deletes" \
     --set "auto-soft-delete-request-criteria:All Delete Requests" \
     --set "soft-delete-retention-time:30 d"

dsconfig set-global-configuration-prop \
     --set "soft-delete-policy:Automatic Soft Deletes"

The Soft and Hard Delete Controls

The Soft Delete Request Control

If you want to explicitly control which delete requests get turned into soft deletes, then you can include the soft delete request control in the delete request. This request control has an OID of “1.3.6.1.4.1.30221.2.5.20”, and it can optionally have a value. If there is a value, then it should have the following ASN.1 encoding:

SoftDeleteRequestValue ::= SEQUENCE {
     returnSoftDeleteResponse     [0] BOOLEAN DEFAULT TRUE,
     ... }

The Soft Delete Response Control

If the request control doesn’t have a value, or if it has a value with the returnSoftDeleteResponse flag set to true, then the delete result may include a soft delete response control with an OID of “1.3.6.1.4.1.30221.2.5.21” and whose value is simply the string representation of the DN for the soft-deleted entry. The soft-deleted entry DN will be the same as the original DN, but with the RDN updated to include the entry’s entryUUID attribute value. For example, if the entry “uid=jdoe,ou=People,dc=example,dc=com” has an entryUUID value of “53e84e32-4be9-4ed6-b489-88d8bea4bdcd”, then the resulting DN for the soft-deleted entry would be “entryUUID=53e84e32-4be9-4ed6-b489-88d8bea4bdcd+uid=jdoe,ou=People,dc=example,dc=com”. The soft-deleted entry will also include the ds-soft-delete-entry object class, and it will include a ds-soft-delete-from-dn attribute whose value was the DN of the original entry and a ds-soft-delete-timestamp attribute whose value reflects the time that the soft delete operation was performed.

The Hard Delete Request Control

We also offer a hard delete request control, which can be used to explicitly indicate that an entry should be completely removed, even if the server would have otherwise automatically turned the delete operation into a soft delete. The hard delete request control has an OID of “1.3.6.1.4.1.30221.2.5.22” and no value. There is no corresponding hard delete response control.

Using the Soft and Hard Delete Controls With ldapmodify

The ldapmodify command-line tool offers support for the soft delete request control via the “--softDelete” argument, and for the hard delete request control via the “--hardDelete” argument.

For example, the following can be used to remove the “uid=jdoe,ou=People,dc=example,dc=com” entry using a soft delete operation:

$ bin/ldapmodify --hostname ldap.example.com \
     --port 636 \
     -useSSL \
     --trustStorePath config/truststore \
     --bindDN "uid=admin,dc=example,dc=com" \
     --softDelete
Enter the bind password:

# Successfully connected to ldap.example.com:636.

dn: uid=jdoe,ou=People,dc=example,dc=com
changetype: delete

# Deleting entry uid=jdoe,ou=People,dc=example,dc=com ...
# Result Code:  0 (success)
# Soft Delete Response Control:
#      OID:  1.3.6.1.4.1.30221.2.5.21
#      Soft-Deleted Entry DN:  entryUUID=53e84e32-4be9-4ed6-b489-88d8bea4bdcd+uid=jdoe,ou=People,dc=example,dc=com

Using the Soft and Hard Delete Controls With the UnboundID LDAP SDK for Java

The UnboundID LDAP SDK for Java supports the soft delete controls via the SoftDeleteRequestControl and the SoftDeleteResponseControl classes. It supports the hard delete request control via the HardDeleteRequestControl class. The class-level Javadoc documentation for the SoftDeleteRequestControl includes an example that demonstrates the use of these controls, and the related undelete and soft-deleted entry access request controls.

The Soft-Deleted Entry Access Request Control

There wouldn’t be much benefit to having soft-deleted entries if we didn’t provide a way to get access to them. By default, soft-deleted entries are hidden from clients, so they won’t be included in search results. However, there are two ways that you can access soft-deleted entries:

  • If you know the soft-deleted entry’s DN, then you can retrieve that entry with a search using the baseObject scope.
  • If you issue a search with the soft-deleted entry access request control, then soft-deleted entries can be included in the search results.

The latter option is much more useful than the former because it’s hard to know what a soft-deleted entry’s DN is unless you knew that entry’s UUID value before it was deleted, or you got the soft-deleted entry DN from the soft delete response control.

The soft-deleted entry access request control has an OID of “1.3.6.1.4.1.30221.2.5.24”. It may optionally have a value, and if it does, then that value must have the following ASN.1 encoding:

SoftDeleteAccessRequestValue ::= SEQUENCE {
     includeNonSoftDeletedEntries     [0] BOOLEAN DEFAULT TRUE,
     returnEntriesInUndeletedForm     [1] BOOLEAN DEFAULT FALSE,
     ... }

The includeNonSoftDeletedEntries element of the request control indicates whether the server should include non-soft-deleted entries in the search results. If this is true (which is the default), then the set of entries returned may include both soft-deleted and non-soft-deleted entries. If this is false, then only soft-deleted entries will be returned.

The returnEntriesInUndeletedForm element of the request control indicates whether matching soft-deleted entries should be returned in their undeleted form (if true) rather than their soft-deleted form (if false). The main difference between these forms is that the undeleted form will have the entry’s original DN rather than the soft-deleted DN that includes the entryUUID attribute, and will not include the ds-soft-delete-entry object class or the ds-soft-delete-from-dn or ds-soft-delete-timestamp attributes.

If the request control doesn’t have a value, then it will behave as if you provided a value with includeNonSoftDeletedEntries set to true and returnEntriesInUndeletedForm set to false.

Using the Soft-Deleted Entry Access Request Control With ldapsearch

The ldapsearch command-line tool offers support for the soft-deleted entry access request control through the --includeSoftDeletedEntries argument. This argument must take a value, and that value should be one of the following:

  • with-non-deleted-entries — Indicates that both soft-deleted and non-soft-deleted entries should be included in the search results. Soft-deleted entries will be returned in their soft-deleted form.
  • without-non-deleted-entries — Indicates that only soft-deleted entries should be returned, in their soft-deleted form. Non-soft-deleted entries will not be returned.
  • deleted-entries-in-undeleted-form — Indicates that only soft-deleted entries should be returned, but they should be returned in their undeleted form.

For example, if you wanted to search for the soft-deleted entry with a uid value of jdoe, you could use a command like:

$ bin/ldapsearch --hostname ldap.example.com \
     --port 636 \
     --useSSL \
     --trustStorePath config/truststore \
     --bindDN "uid=admin,dc=example,dc=com" \
     --baseDN "dc=example,dc=com" \
     --scope sub \
     --requestedAttribute "*" \
     --requestedAttribute "+" \
     --includeSoftDeletedEntries without-non-deleted-entries \
     "(uid=jdoe)"
Enter the bind password:

# Soft Delete Response Control:
#      OID:  1.3.6.1.4.1.30221.2.5.21
#      Soft-Deleted Entry DN:  entryUUID=53e84e32-4be9-4ed6-b489-88d8bea4bdcd+uid=jdoe,ou=People,dc=example,dc=com
dn: entryUUID=53e84e32-4be9-4ed6-b489-88d8bea4bdcd+uid=jdoe,ou=People,dc=example,dc=com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: ds-soft-delete-entry
sn: Doe
cn: John Doe
givenName: John
uid: jdoe
createTimestamp: 20190227170715.814Z
creatorsName: cn=Directory Manager,cn=Root DNs,cn=config
modifyTimestamp: 20190227170715.814Z
modifiersName: cn=Directory Manager,cn=Root DNs,cn=config
entryUUID: 53e84e32-4be9-4ed6-b489-88d8bea4bdcd
ds-soft-delete-from-dn: uid=jdoe,ou=People,dc=example,dc=com
ds-soft-delete-timestamp: 20190227170734.870Z
ds-entry-checksum: 2440630426
subschemaSubentry: cn=schema

# Result Code:  0 (success)
# Number of Entries Returned:  1

Using the Soft-Deleted Entry Access Request Control With the UnboundID LDAP SDK for Java

The UnboundID LDAP SDK for Java provides support for the soft-deleted entry access request control through the SoftDeletedEntryAccessRequestControl class. The class-level Javadoc documentation for the SoftDeleteRequestControl class provides an example that demonstrates how to use this control (along with the soft delete, hard delete, and undelete request controls).

The Undelete Request Control

Support for soft deletes would also not be very useful if we didn’t provide a way to restore a soft-deleted entry back to being a regular, non-soft-deleted entry. And we do offer that ability through the undelete request control. If you include this control in a specially crafted add request, then the server will restore the target entry back to its former glory. The undelete request control has an OID of “1.3.6.1.4.1.30221.2.5.23”, and it does not need a value. There is no corresponding response control.

Note that I mentioned a “specially crafted add request” in that last paragraph. The server handles the undelete operation as an add operation, but if the undelete request control is present, then the contents of that add request will be a little different from when you’re adding an entry from scratch. Here’s what you need to include:

  • The DN included in the add request should be the DN that you want the undeleted to have. If you want this to be the entry’s original DN, then you could use the value of the soft-deleted entry’s ds-soft-delete-from-dn attribute, but you can choose something else if you want the restored entry to have a different DN.
  • The add request must include a ds-undelete-from-dn attribute whose value is the DN of the soft-deleted entry that you want to undelete.

Using the Undelete Request Control With ldapmodify

The ldapmodify tool supports the use of the undelete request control through the “--allowUndelete” argument. If you add this argument, then the undelete request control will automatically be included in any add requests that it sends. For example:

$ bin/ldapmodify --hostname ldap.example.com \
     --port 636 \
     -useSSL \
     --trustStorePath config/truststore \
     --bindDN "uid=admin,dc=example,dc=com" \
     --allowUndelete
Enter the bind password:

# Successfully connected to ldap.example.com:636.

dn: uid=jdoe,ou=People,dc=example,dc=com
changetype: add
ds-undelete-from-dn: entryUUID=53e84e32-4be9-4ed6-b489-88d8bea4bdcd+uid=jdoe,ou=People,dc=example,dc=com

# Adding entry uid=jdoe,ou=People,dc=example,dc=com ...
# Result Code:  0 (success)

Using the Undelete Request Control with the UnboundID LDAP SDK for Java

The UnboundID LDAP SDK for Java supports the undelete request control via the UndeleteRequestControl class. This class even provides a helpful createUndeleteRequest convenience method that allows you to construct an appropriate add request when provided with the DN that you want the undeleted entry to have and the DN of the soft-deleted entry that you want to undelete. As noted above, the class-level Javadoc documentation for the SoftDeleteRequestControl class provides an example that demonstrates how to use all of the controls related to soft-delete processing.

The Multi-Update Extended Operation in the Ping Identity Directory Server

In an earlier post, I mentioned that while you can process multiple searches in parallel to speed up an application that needs multiple pieces of information to do its work, that generally only works if those searches are independent. If the searches are related (meaning, that you need the results of one to construct another request), then this approach won’t work. Fortunately, the Ping Identity Directory Server offers an LDAP join control that allows you to perform a search that not only retrieves the entries matching the search criteria, but that also joins those entries with related entries as specified by the join rule.

Wouldn’t it be nice if there were something similar for write operations? While LDAP allows you to send multiple write requests concurrently on the same or different connections, you can’t really do that if there are dependencies between those write operations. For example, let’s say that you want to add an entry along with some subordinate entries. You can’t add a child before creating its parent, and if you try to send them in parallel, then maybe it’ll work and maybe it won’t. But here again, the Ping Identity Directory Server has you covered, this time in the form of the multi-update extended operation.

As its name implies, the multi-update operation allows you to send multiple updates (any combination of add, delete, modify, modify DN, and password modify extended operations) in a single request that will be processed in the order that you provide them. At the very least, this allows you to reduce the amount of time required to process those operations because there’s only one round trip between the client and the server. But it also allows you to decide what happens if an error occurs while processing any of those updates, and here you have three options:

  • You can have the processing occur atomically so that no changes will be applied unless all of them are processed successfully, and so that no client will be able to see the data in an intermediate state with only some of the changes completed. You can get this same benefit from LDAP transactions as described in RFC 5805 (which the Ping Identity Directory Server also supports), but the multi-update operation is more efficient because there’s only a single request and response, whereas LDAP transactions require a separate network round trip for each of the changes, plus additional round trips when starting and ending the transaction.
  • You can have processing stop after the first error. Any writes that succeeded before the error will be preserved, but any changes in the multi-update request after the one that caused the error will be ignored. Clients may be able to see the data in an intermediate state while these operations are being processed.
  • You can have processing continue until all of the operations have been attempted. Any of the changes that are successful will remain in place, and again, clients may be able to see the data in an intermediate state while they are being processed.

The Multi-Update Extended Request

The multi-update extended request has an OID of 1.3.6.1.4.1.30221.2.6.17 and a value with the following ASN.1 encoding:

MultiUpdateRequestValue ::= SEQUENCE {
     errorBehavior     ENUMERATED {
          atomic              (0),
          quitOnError         (1),
          continueOnError     (2),
          ... },
     requests          SEQUENCE OF SEQUENCE {
          updateOp     CHOICE {
               modifyRequest     ModifyRequest,
               addRequest        AddRequest,
               delRequest        DelRequest,
               modDNRequest      ModifyDNRequest,
               extendedReq       ExtendedRequest,
               ... },
          controls     [0] Controls OPTIONAL,
          ... },
     ... }

As you might expect, the request just specifies the behavior to use in case an error is encountered during processing and the set of requests to be processed. Note that while the ASN.1 definition above does allow for any kind of extended request to be included, the only one that the Ping Identity Directory Server currently allows in a multi-update request in the password modify extended request.

The UnboundID LDAP SDK for Java offers support for the multi-update extended request through the MultiUpdateExtendedRequest class, with an assist from the MultiUpdateErrorBehavior enum. If you want to use the multi-update extended operation through some other API, you’ll need to encode the request for yourself.

The Multi-Update Extended Result

The multi-update extended result has an OID of 1.3.6.1.4.1.30221.2.6.18 and a value with the following encoding:

MultiUpdateResultValue ::= SEQUENCE {
     changesApplied     ENUMERATED {
          none        (0),
          all         (1),
          partial     (2),
     ... },
     responses     SEQUENCE OF SEQUENCE {
          responseOp     CHOICE {
               modifyResponse     ModifyResponse,
               addResponse        AddResponse,
               delResponse        DelResponse,
               modDNResponse      ModifyDNResponse,
               extendedResp       ExtendedResponse,
               ... },
          controls       [0] Controls OPTIONAL,
          ... },
     ... }

There are two components to the extended result value:

  • An indicator as to whether none, all, or some of the changes were applied.
  • The results for all of the operations that were attempted. The results will be listed in the same order as in the request, and each operation result may optionally include the response controls for that operation.

If only a portion of the operations were attempted (for example, because the server stopped processing the multi-update operation after an error was encountered while processing one of the changes and did not attempt any of the others after that), then there may be fewer results than there were requests.

The UnboundID LDAP SDK for Java offers support for the multi-update extended result through the MultiUpdateExtendedResult class and the MultiUpdateChangesApplied enum. To use this extended operation in another API, you’ll need to decode the result value on your own.

Supported Controls

There are two categories of controls that may be used in conjunction with the multi-update extended request: those that can be attached to the multi-update extended operation itself, and those that can be attached to the individual operation requests inside the multi-update request value.

The controls that may be attached to the multi-update extended operation itself are:

  • Get Backend Set ID (only for atomic requests)
  • Intermediate Client
  • Proxied Authorization v1
  • Proxied Authorization v2
  • Route To Backend Server (only for atomic requests)
  • Transaction Settings (only for atomic requests)

The controls that may be attached to operation requests inside the multi-update request value are:

  • Account Usable
  • Assertion
  • Intermediate Client
  • Get Backend Set ID (only for non-atomic requests)
  • Hard Delete
  • Manage DSA IT
  • Password Policy
  • Post-Read
  • Pre-Read
  • Replication Repair
  • Route To Backend Server (only for non-atomic requests)
  • Soft Delete
  • Subtree Delete
  • Undelete

An Example Using the UnboundID LDAP SDK for Java

The ldapmodify tool provided as part of the UnboundID LDAP SDK for Java already includes support for the multi-update extended operation (via the --multiUpdateErrorBehavior argument), and you can find the code for that tool at https://github.com/pingidentity/ldapsdk/blob/master/src/com/unboundid/ldap/sdk/unboundidds/tools/LDAPModify.java.

However, that version of ldapmodify has a lot of features, and the multi-update support is only a tiny portion of it. For the sake of clearer illustration, I wrote a much simpler command-line tool that serves as a clearer demonstration of the multi-update operation. It operates much like ldapmodify, but it only reads the changes from an LDIF file, and it sends them to the server all at once through a multi-update operation. You can find that example at https://github.com/dirmgr/blog-example-source-code/tree/master/multi-update.

The LDAP Join Control in the Ping Identity Directory Server

LDAP is an asynchronous protocol, and most directory servers are very good when it comes to handling multiple requests simultaneously, whether on the same connection or multiple connections. If an application needs to issue multiple independent searches in the course of performing some function, and if it wants to get the results in as little time as possible, then it can issue those searches concurrently rather than sequentially. This means that the application should have all the results it needs in the time required to process the longest operation, as opposed to the sum of the times required to process those operations (not to mention the network round-trip time, which gets magnified when issuing requests in series rather than in parallel).

However, you can’t use this approach if there are dependencies between the searches that you want to perform. For example, let’s say that when an employee logs in, you want to show them a portion of their organizational chart that includes their manager and their peers (that is, the other employees who share the same manager). Normally, you’d need to perform three searches:

  • One search to retrieve the entry for the user who is authenticating and get the manager attribute
  • One to retrieve the entry for the user referenced by the employee’s manager attribute
  • One to retrieve the entries of all users with that same manager value

While you could potentially issue the second and third searches concurrently, you have to wait for the results of the first search to have the information you need for the other two.

In the Ping Identity Directory Server, we provide support for a feature that can allow you to do all of this with a single request: the LDAP join control. As its name implies, it offers an LDAP take on the SQL join you can perform in a relational database. If you issue a search request that includes the join request control, then each entry that matches the search criteria will be joined with “related” entries (in accordance with the criteria in the join request control).

The Join Request Control

The join request control describes the relationship that you want to use to identify other entries that are in some way related to the entries matching your search request. The request control has an OID of 1.3.6.1.4.1.30221.2.5.9 and a value with the following ASN.1 encoding:

LDAPJoin ::= SEQUENCE {
     joinRule         JoinRule,
     baseObject       CHOICE {
          useSearchBaseDN      [0] NULL,
          useSourceEntryDN     [1] NULL,
          useCustomBaseDN      [2] LDAPDN,
          ... },
     scope            [0] ENUMERATED {
          baseObject             (0),
          singleLevel            (1),
          wholeSubtree           (2),
          subordinateSubtree     (3),
          ... } OPTIONAL,
     derefAliases     [1] ENUMERATED {
          neverDerefAliases       (0),
          derefInSearching        (1),
          derefFindingBaseObj     (2),
          derefAlways             (3),
          ... } OPTIONAL,
     sizeLimit        [2] INTEGER (0 .. maxInt) OPTIONAL,
     filter           [3] Filter OPTIONAL,
     attributes       [4] AttributeSelection OPTIONAL,
     requireMatch     [5] BOOLEAN DEFAULT FALSE,
     nestedJoin       [6] LDAPJoin OPTIONAL,
     ... }

JoinRule ::= CHOICE {
     and               [0] SET (1 .. MAX) of JoinRule,
     or                [1] SET (1 .. MAX) of JoinRule,
     dnJoin            [2] AttributeDescription,
     equalityJoin      [3] JoinRuleAssertion,
     containsJoin      [4] JoinRuleAssertion,
     reverseDNJoin     [5] AttributeDescription,
     ... }

JoinRuleAssertion ::= SEQUENCE {
     sourceAttribute     AttributeDescription,
     targetAttribute     AttributeDescription,
     matchAll            BOOLEAN DEFAULT FALSE }

Most of the fields of the join request should be familiar because they’re similar to the fields of an ordinary search request. But I’ll go ahead and call them all out anyway. Also note that the Javadoc for the JoinRequestControl, JoinRequestValue, JoinRule, and JoinBaseDN classes in the UnboundID LDAP SDK for Java may provide additional information.

The Join Rule

The first, and probably most important, field of a join request control is the join rule. This is used to specify the types of entries that should be joined with the corresponding search result entry. We currently offer six types of join rules (and may add support for more in the future):

  • AND — An AND join rule encapsulates a set of one or more other join rules and will only join a search result entry with entries that match the criteria for all of the encapsulated join rules.
  • OR — An OR join rule encapsulates a set of one or more other join rules and will only join a search result entry with entries that match the criteria for at least one of the encapsulated join rules.
  • DN Join — A DN join rule will join a search result entry with other entries whose DNs are contained in a specified attribute in the search result entry. For example, You could use a DN join rule to join a groupOfNames entry with the entries whose DNs are contained in the member attribute of the group. Or you could join an employee’s entry with the entry of their boss via the manager attribute in the employee’s entry.
  • Equality Join — An equality join rule will join a search result entry with other entries that share a common attribute value. For example, say that a mobile phone service provider has an entry for each account, and an entry for each device linked to an account. If the account entry has an accountNumber attribute, and the devices associated with that account also contain that same accountNumber value, you could use an equality join to associate the device entries with the account. Also note that the value that the joined entries have in common doesn’t necessarily have to be in the same attribute type (for example, it could be in the accountNumber attribute of an account entry and the deviceAccountNumber attribute of a device entry).
  • Contains Join — A contains join rule is much like an equality join rule, except that the server uses a substring match (and more correctly, a subAny match) instead of an equality match when identifying the entries to join with the search result entry. That is, the value of a specified attribute in the search result entry must be equal to or a substring of a value in a specified attribute in the joined entries.
  • Reverse DN Join — A reverse DN join rule will join a search result entry with entries that contain the DN of that search result entry in the value of a specified attribute. For example, you could use a reverse DN join to retrieve the entries for a user’s direct reports via the manager attribute.

The Join Base DN

The base DN field of a join request is expressed a little bit differently than the base DN from a search request. In a search request, you must always specify the topmost entry in the subtree containing the entries you’re interested in retrieving. The base DN field of a join request has the same purpose, but there are three different ways that you can indicate what that base DN should be:

  • You can indicate that the base DN from the search request should also be the base DN used when finding entries to be joined with each search result entry.
  • You can indicate that the DN of each search result entry should be used as the base DN used when finding entries to be joined with that search result entry.
  • You can explicitly specify the base DN that you want to use for the join request.

The Join Scope

The scope field of a join request has basically the same meaning as the scope field of a search request, except that it’s relative to the join base DN rather than the search base DN. Those scopes are:

  • baseObject — This indicates that only the entry specified by the join base DN may be joined with the search result entry. None of its subordinates will be included.
  • singleLevel — This indicates that only the entries that are the immediate subordinates of the entry specified by the join base DN may be joined with the search result entry. The join base entry itself will not be included, nor will entries more than one level below the join base entry.
  • wholeSubtree — This indicates that the join base entry and all of its subordinates, to any depth, may be joined with the search result entry.
  • subordinateSubtree — This indicates that all entries below the join base entry, to any depth, may be joined with the search result entry. The join base entry itself will not be included.

Note that the scope element of a join request control is optional, and if it is not provided, then the scope from the search request will be used.

The Join Alias Dereferencing Policy

The alias dereferencing policy in the join request control has the same meaning as in the search request itself. It tells the server how it should treat any aliases that are encountered during join processing. The allowed values include:

  • neverDerefAliases — Indicates that the server should not attempt to dereference any aliases encountered during join processing.
  • derefInSearching — Indicates that the server should attempt to dereference any aliases encountered below the join base DN, but not the join base DN itself if it happens to be an alias.
  • derefFindingBaseObj — Indicates that the server should attempt to dereference the join base DN if it happens to be an alias, but not any aliases encountered below that entry.
  • derefAlways — Indicates that the server should attempt to dereference any aliases it encounters during join processing.

As with the join scope, this is an optional element in a join request control. If you leave it out, the server will use the same policy as specified in the search request.

The Join Size Limit

This specifies the maximum number of entries that should be joined with each search result entry. If a search result entry would have been joined with more than 1000 entries, then the join result control associated with that entry will include a “size limit exceeded” join result code and will not include any of the joined entries.

Note that the server may impose a size limit that is lower than the one requested by the client. In particular, the effective size limit, cannot be larger than the requester’s maximum search size limit, the maximum size limit imposed by the requester’s client connection policy, and the maximum join request size limit defined in the global configuration.

This is an optional element, and if it is not specified, then the size limit from the search request will be used as the size limit for the join processing.

On a related note, the join request control does not include an element to specify a time limit. The time limit from the search request applies to the entire set of processing for the request.

The Join Filter

This is an optional additional search filter that will be required to match any entry for it to be joined with a search result entry. By default, all entries within the scope of the join request that match the criteria specified by the join rule will be joined with the search result entry. If an additional filter is specified, then entries will also have to match that filter to be included in the join.

The Join Attributes

This specifies the set of attributes that should be included in the entries that are joined with a search result entry. This works in the same way as the set of requested attributes for the search request itself, and it can include special tokens like “*” (to indicate that all user attributes should be included), “+” (to indicate that all operational attributes should be included), and “@” followed by an object class name (to indicate that all attributes associated with that object class should be included). If this element is missing or empty, then the default behavior will be to return all user attributes.

Note that the set of attributes that will actually be included in joined entries may be less than the set of requested attributes. For example, some attributes may be excluded because the requester does not have access control permission to retrieve them, or because returning them would violate a sensitive attribute constraint.

The Require Match Flag

This indicates whether a search result entry must be joined with at least one other entry for it to be included in the search results. If this is set to true and there are no entries that match the join criteria for a given search result entry, then that search result entry will not be returned to the client. If this is false, or if it is omitted from the join request control, then the search result entry will still be returned.

The Nested Join Element

The nested join element allows you to extend the join processing to more levels. Not only can you join each search result entry with a related set of entries, but you can also join each of those joined entries with even more related entries based on another set of join criteria. Note that if there is a nested join element, then each joined entry effectively becomes the search result entry for the next level of the join. And if you want to have more than two levels of joins, nested joins can include their own nested joins, but you probably don’t want to go too deep because it has the potential to make the join result really big.

For example, in the scenario we described above where you want to join an employee to their manager and peers, the search request could be used to find the employee’s entry, and you could use a DN join to join it with their manager’s entry (based on the manager attribute in the employee’s entry), and then you could use a reverse DN join to join the manager with their direct reports (also by the manager attribute in their entries).

The Join Result Control

If the search request includes a join request control, then each search result entry will include a join result control that provides information about the join processing for that entry. The join result control has an OID of 1.3.6.1.4.1.30221.2.5.9 and a value with the following encoding:

JoinResult ::= SEQUENCE {
     COMPONENTS OF LDAPResult,
     entries     [4] SEQUENCE OF JoinedEntry }

JoinedEntry ::= SEQUENCE {
     objectName            LDAPDN,
     attributes            PartialAttributeList,
     nestedJoinResults     SEQUENCE OF JoinedEntry OPTIONAL }

So basically, the join result control contains a result code, an optional diagnostic message, an optional matched DN, an optional set of referral URLs, and a list of the entries that were joined with the search result entry. And if the join request included a nested join, then each joined entry can have its own set of joined entries.

See the JoinResultControl and JoinedEntry classes in the UnboundID LDAP SDK for Java for more information.

An Example Using the ldapsearch Tool

The ldapsearch command-line tool shipped with the Ping Identity Directory Server (and also with the UnboundID LDAP SDK for Java) includes support for the LDAP join control through the following arguments:

  • --joinRule — The join rule to use. This is the only argument that is required to include the join request control in the search request. The value must be in one of the following formats:

    • dn:{sourceAttribute} — Indicates that each search result entry should be joined with entries whose DNs are contained in the specified source attribute of the search result entry.
    • reverse-dn:{targetAttribute} — Indicates that each search result entry should be joined with entries that contain the DN of the search result entry in the specified target attribute.
    • equals:{sourceAttribute}:{targetAttribute} — Indicates that each search result entry should be joined with entries that have the value of the search result entry’s source attribute in the joined entry’s target attribute.
    • contains:{sourceAttribute}:{targetAttribute} — Indicates that each search result entry should be joined with entries that contain the value of the search result entry’s source attribute as a substring in the joined entry’s target attribute.
  • --joinBaseDN — The join base DN to use. If this is omitted, then the base DN from the search request will be used. If it is provided, the value can be one of the following:

    • The string “search-base”, which indicates that the base DN of the search request should also be used as the join base DN.
    • The string “source-entry-dn”, which indicates that the DN of the search result entry should be used as the join base DN.
    • Any valid LDAP DN, which will be used as the join base DN.
  • --joinScope — The scope for the join processing. If this is omitted, then the scope from the search request will be used. If it is provided, then the value may be one of the following:

    • base — The baseObject scope.
    • one — The singleLevel scope.
    • sub — The wholeSubtree scope.
    • subordinates — The subordinateSubtree scope.
  • --joinSizeLimit — The maximum number of entries that should be joined with each search result entry. If this is omitted, then the search request size limit will be used.
  • --joinFilter — An additional filter that joined entries will be required to match. If this is omitted, then no additional filter will be used.
  • --joinRequestedAttribute — The name or OID of an attribute that should be included in joined entries. This can be specified multiple times to indicate that multiple attributes should be included. If this is omitted, then all user attributes will be requested.
  • --joinRequireMatch — If present, this indicates that search result entries that aren’t joined with any entries should be omitted from the results.

As you might have noticed, the ldapsearch tool doesn’t quite provide full support for all of the LDAP join features. For example, it doesn’t offer the AND or OR join rule types, and it doesn’t allow you to perform a nested join. But it’s still good enough to let you try out the join control in a number of common cases.

The following is an example that demonstrates using ldapsearch to perform a DN join that links an employee’s entry to the entry of their boss via the manager attribute:

$ bin/ldapsearch --hostname ds.example.com \
     --port 636 \
     --useSSL \
     --bindDN 'cn=LDAP Join Example,ou=Applications,dc=example,dc=com' \
     --joinRule dn:manager \
     --joinBaseDN search-base \
     --joinScope sub \
     --joinRequestedAttribute givenName \
     --joinRequestedAttribute sn \
     --joinRequestedAttribute mail \
     --joinRequestedAttribute telephoneNumber \
     --baseDN dc=example,dc=com \
     --scope sub "(uid=ernest.employee)" \
     givenName \
     sn \
     mail \
     telephoneNumber
Enter the bind password:

The server presented the following certificate chain:

     Subject: CN=ds.example.com,O=Ping Identity Self-Signed Certificate
     Valid From: Friday, February 8, 2019 at 12:23:34 AM CST
     Valid Until: Friday, February 4, 2039 at 12:23:34 AM CST
     SHA-1 Fingerprint: 78:f1:49:a0:06:0b:bd:1e:2c:88:cb:76:60:cb:87:cb:c4:c3:76:97
     256-bit SHA-2 Fingerprint: 55:9c:9a:54:97:48:8c:51:fa:10:da:a0:08:f0:15:dc:f0:92:75:3e:e9:be:56:c5:5c:5c:ec:d5:d4:85:15:a2

WARNING:  The certificate is self-signed.

Do you wish to trust this certificate?  Enter 'y' or 'n': y
# Join Result Control:
#      OID:  1.3.6.1.4.1.30221.2.5.9
#      Join Result Code:  0 (success)
#      Joined With Entry:
#           dn: uid=betty.boss,ou=People,dc=example,dc=com
#           mail: betty.boss@example.com
#           sn: Boss
#           givenName: Betty
#           telephoneNumber: +1 123 456 7891
dn: uid=ernest.employee,ou=People,dc=example,dc=com
mail: ernest.employee@example.com
sn: Employee
givenName: Ernest
telephoneNumber: +1 123 456 7890

# Result Code:  0 (success)
# Number of Entries Returned:  1

An Example Using the UnboundID LDAP SDK for Java

I’ve also written an example that demonstrates the use of the LDAP join control in the UnboundID LDAP SDK for Java. The LDAP SDK does support using nested joins, so this example uses the scenario outlined above, in which we retrieve a user, their manager, and their peers. You can find it at https://github.com/dirmgr/blog-example-source-code/tree/master/ldap-join.

The Get Password Policy State Issues Control in the Ping Identity Directory Server

In the Ping Identity Directory Server, we’re very serious when it comes to security. We make it easy to encrypt all your data, including the database contents (and the in-memory database cache), network communication, backups, LDIF exports, and even log files. We’ve got lots of password policy features, like strong password encoding, many password validation options, and ways to help thwart password guessing attempts. We offer several two-factor authentication options. We have a powerful access control subsystem that is augmented with additional features like sensitive attributes and privileges. We have lots of monitoring and alerting features so that you can be notified of any problems as soon as (or, in many cases, before) they arise so that your service remains available. Security was a key focus back when I started writing OpenDS (which is the ancestor of the Ping Identity Directory Server), and it’s still a key focus today.

One small aspect of this focus on security is that, by default, we don’t divulge any information about the reason for a failed authentication attempt. Maybe the account doesn’t exist, or maybe it’s locked or administratively disabled. Maybe the password was wrong, or maybe it’s expired. Maybe the user isn’t allowed to authenticate from that client system. In all of these cases, and for other types of authentication failures, the server will just return a bind result with a result code of invalidCredentials and no diagnostic message. The server will include the exact reason for the authentication failure in the audit log so that it’s available for administrators, but we won’t return it to the client so that a malicious user can’t use that to better craft their attack.

Now, if you don’t care about this and want the server to just go ahead and provide the message to the client, then you can do that with the following configuration change:

dsconfig set-global-configuration-prop --set return-bind-error-messages:true

However, that may not be the best option because it applies equally to all authentication requests for all clients, and because the output is human-readable but not very machine parseable. It’s not easy for a client to programmatically determine what the reason for the failure is. For that, your best option is the get password policy state issues control.

The get password policy state issues control indicates that you want the server to return information about the nature of the authentication failure, and details of the user’s password policy state that might interfere with authentication either now or in the future. This information is easy to consume programmatically, but it also contains user-friendly representations of those conditions as well. We intend for this control to be used by applications that authenticate users, and that can decide what information they want to make available to the end user.

Restrictions Around the Control’s Use

As previously mentioned, we might not always want to divulge the reason for a failed authentication attempt to the end user. As such, if we allowed just anyone to use this control, then that would get thrown out the window since a malicious client could just always include that control and get some helpful information in the response. So we don’t do that. Instead, this control will only be permitted if all of the following conditions are met:

  • The server’s access control handler must allow the get password policy state issues request control to be included in bind requests. This control is allowed in bind request by default, but you can disable it if you want to.
  • A bind request that includes the get password policy state issues request control must be received on a connection that is already authenticated as a user who has the permit-get-password-policy-state-issues privilege.

Since we intend this feature to be used by applications that authenticate users, we expect that any application that is to be authorized to use it will have an account with the necessary privilege. And since the get password policy state issues control is a proprietary feature, we expect that any application that knows how to use it can also easily include the retain identity request control in those same bind requests.

The Get Password Policy State Issues Request Control

The get password policy state issues request control is very simple: it’s got a request OID of 1.3.6.1.4.1.30221.2.5.46 and no value. This control is only intended to be included in bind requests, and it’s really just asking the server to include the corresponding response control in the bind result message.

It’s easy enough to use this request control any LDAP API, but if you’re using the UnboundID LDAP SDK for Java, then we provide the GetPasswordPolicyStateIssuesRequestControl class to make it even easier.

The Get Password Policy State Issues Response Control

The get password policy state issues response control is more complicated than the request control. It has an OID of 1.3.6.1.4.1.30221.2.5.47 and a value with the following ASN.1 encoding:

GetPasswordPolicyStateIssuesResponse ::= SEQUENCE {
     notices               [0] SEQUENCE OF SEQUENCE {
          type        INTEGER,
          name        OCTET STRING,
          message     OCTET STRING OPTIONAL } OPTIONAL,
     warnings              [1] SEQUENCE OF SEQUENCE {
          type        INTEGER,
          name        OCTET STRING,
          message     OCTET STRING OPTIONAL } OPTIONAL,
     errors                [2] SEQUENCE OF SEQUENCE {
          type        INTEGER,
          name        OCTET STRING,
          message     OCTET STRING OPTIONAL } OPTIONAL,
     authFailureReason     [3] SEQUENCE {
          type        INTEGER,
          name        OCTET STRING,
          message     OCTET STRING OPTIONAL } OPTIONAL,
     ... }

If you’re using the UnboundID LDAP SDK for Java, then you can use the GetPasswordPolicyStateIssuesResponseControl class to do all the heavy lifting for you. If you’re using some other API, then you’ll probably have to decode the value for yourself.

There are four basic components to the get password policy state issues response control:

  • A set of error conditions in the user’s password policy state that will either prevent that user from authenticating, or that will prevent them from using their account until they take some action. In the UnboundID LDAP SDK for Java, this we offer the PasswordPolicyStateAccountUsabilityError class to make it easier to interpret these errors. Possible password policy state error conditions include:

    • The account is administratively disabled.
    • The account has expired.
    • The account is not yet active.
    • The account is permanently locked (or at least until an administrator unlocks it) after too many failed authentication attempts.
    • The account is temporarily locked after too many failed authentication attempts.
    • The account is locked because it’s been idle for too long.
    • The account is locked because the password was administratively reset, but the user didn’t choose a new password quickly enough.
    • The password is expired.
    • The password is expired, but there are one or more grace logins remaining. Authenticating with a grace login will only permit them to bind for the purpose of changing the password.
    • The password has been administratively reset and must be changed before the user will be allowed to do anything else.
    • The password policy was configured so that all users governed by that policy must change their passwords by a specified time, but the user attempting to authenticate failed to do so.
  • A set of warning conditions in the user’s password policy state that won’t immediately impact their ability to use their account, but that may impact their ability to use the account in the near future unless they take some action. In the UnboundID LDAP SDK for Java, we offer the PasswordPolicyStateAccountUsabilityWarning class to make it easier to interpret these warnings. Possible password policy state warning conditions include:

    • The account will expire in the near future.
    • The password will expire in the near future.
    • The account has been idle for too long and will be locked unless they successfully authenticate in the near future.
    • The account has outstanding authentication failures and may be locked if there are too many more failed attempts.
    • The password policy was configured so that all users governed by that policy must change their password by a specified time, but the user attempting to authenticate has not yet done so.
  • A set of notice conditions that additional information about the user’s password policy state that may be helpful for applications or the end user to know. The UnboundID LDAP SDK for Java provides the PasswordPolicyStateAccountUsabilityNotice class to make it easier to interpret these notices. Possible password policy state notices include:

    • A minimum password age has been configured in the password policy governing the user, and it has been less than that length of time since the user last changed their password. The user will not be permitted to change their password again until the minimum age period has elapsed.
    • The account does not have a static password, so it will not be allowed to authenticate using any password-based authentication mechanism.
    • The account has an outstanding delivered one-time password that has not yet been consumed and is not yet expired.
    • The account has an outstanding password reset token that has not yet been consumed and is not yet expired.
    • The account has an outstanding retired password that has not yet expired and may still be used to authenticate.
  • An authentication failure reason, which provides information about the reason that the bind attempt failed. The UnboundID LDAP SDK for Java offers the AuthenticationFailureReason class to help make it easier to use this information. Possible authentication failure reasons include:

    • The server could not find the account for the user that is trying to authenticate (e.g., the user doesn’t exist, or the authentication ID does not uniquely identify the user).
    • The password or other provided credentials were not correct.
    • There was something wrong with the SASL credentials provided by the client (e.g., they were malformed or out of sequence).
    • The account isn’t configured to support the requested authentication type (e.g., they attempted a password-based bind, but the user doesn’t have a password).
    • The account is in an unusable state. The password policy error conditions should encapsulate the reasons that the account is not usable.
    • The server is configured to require the client to authenticate securely, but the authentication attempt was not secure.
    • The account is not permitted to authenticate in the requested manner (e.g., from the client address or using the attempted authentication type).
    • The bind request was rejected by the server’s access control handle.
    • The authentication attempt failed because a problem was encountered while processing one of the controls included in the bind request.
    • The server is currently in lockdown mode and will only permit a limited set of users to authenticate.
    • The server could not assign a client connection policy to the account.
    • The authentication attempt used a SASL mechanism that was implemented in a third-party extension, and that extension encountered an error while processing the bind request.
    • The server encountered an internal error while processing the bind request.

Each password policy state error, warning, and notice, as well as the authentication failure reason, is identified by a name and a numeric type, and also includes a human-readable message suitable for displaying to the user if you decide that it is appropriate.

The Password Policy State Extended Operation

Although it’s not the focus of this blog post (maybe I’ll write another one about it in the future), I should also point out that you can also use the password policy state extended operation to obtain the list of usability errors, warnings, and notices for a user, along with a heck of a lot more information about the state of the account. You can also use it to alter the state if desired. Since it’s an extended operation, you can’t use it in the course of attempting a bind to get the authentication failure reason. However, you could use it in conjunction with the get password policy state issues control if you feel like you need additional state information about the user’s account state after parsing the information in the get password policy state issues response control.

An Example Using the UnboundID LDAP SDK for Java

I’ve written a simple program that demonstrates the use of the get password policy state issues control to obtain the authentication failure reason and password policy state issues for a specified user. You can find that example at https://github.com/dirmgr/blog-example-source-code/tree/master/password-policy-state-issues.

UnboundID LDAP SDK for Java 4.0.9

We have just released version 4.0.9 of the UnboundID LDAP SDK for Java. It is available for download from the releases page of our GitHub repository, from the Files page of our SourceForge repository, and from the Maven Central Repository.

The most significant changes included in this release are:

  • Updated the command-line tool framework to allow tools to have descriptions that are comprised of multiple paragraphs.
  • Updated the support for passphrase-based encryption to work around an apparent JVM bug in the support for some MAC algorithms that could cause them to create an incorrect MAC.
  • Updated all existing ArgumentValueValidator instances to implement the Serializable interface. This can help avoid errors when trying to serialize an argument configured with one of those validators.
  • Updated code used to create HashSet, LinkedHashSet, HashMap, LinkedHashMap, and ConcurrentHashMap instances with a known set of elements to use better algorithms for computing the initial capacity for the map to make it less likely to require the map to be dynamically resized.
  • Updated the LDIF change record API to make it possible to obtain a copy of a change record with a given set of controls.
  • Added additional methods for obtaining a normalized string representation of JSON objects and value components. The new methods provide more control over case sensitivity of field names and string values, and over array order.
  • Improved support for running in a JVM with a security manager that prevents setting system properties (which also prevents access to the System.getProperties method because the returned map is mutable).

UnboundID LDAP SDK for Java 4.0.8

We have just released version 4.0.8 of the UnboundID LDAP SDK for Java. It is available for download from the releases page of our GitHub repository (https://github.com/pingidentity/ldapsdk/releases), from the Files page of our SourceForge repository (https://sourceforge.net/projects/ldap-sdk/files/), and from the Maven Central Repository (https://search.maven.org/search?q=g:com.unboundid%20AND%20a:unboundid-ldapsdk&core=gav).

The most significant changes included in this release are:

  • Fixed a bug in the modrate tool that could cause it to use a fixed string instead of a randomly generated one as the value to use in modifications.
  • Fixed an address caching bug in the RoundRobinDNSServerSet class. An inverted comparison could cause it to use cached addresses after they expired, and to cached addresses that weren’t expired.
  • Updated the ldapmodify tool to remove the restriction that prevented using arbitrary controls with an LDAP transaction or the Ping-proprietary multi-update extended operation.
  • Updated a number of locations in the code that caught Throwable so that they re-throw the original Throwable instance (after performing appropriate cleanup) if that instance was an Error or perhaps a RuntimeException.
  • Added a number of JSONObject convenience methods to make it easier to get the value of a specified field as a string, Boolean, number, object, array, or null value.
  • Added a StaticUtils.toArray convenience method that can be useful for converting a collection to an array when the type of element in the collection isn’t known at compile time.
  • Added support for parsing audit log messages generated by the Ping Identity Directory Server for versions 7.1 and later, including generating LDIF change records that can be used to revert change records (if the audit log is configured to record changes in a reversible form).

UnboundID LDAP SDK for Java 4.0.7

We have just released the UnboundID LDAP SDK for Java version 4.0.7, available for download from the releases page of our GitHub repository, from the Files page of our SourceForge project, and from the Maven Central Repository. The most significant changes in this release include:

  • Fixed an issue in the LDAPConnectionPool and LDAPThreadLocalConnectionPool classes when created with a connection that is already established and authenticated (as opposed to being created from a server set and bind request). Internally, the LDAP SDK created its own server set and bind request from the provided connection’s state information, but it incorrectly included bind credentials in the server set. Under most circumstances, this would merely cause the LDAP SDK to send two bind requests (the second a duplicate of the first) when establishing a new connection as part of the pool. However, it caused a bigger problem when using the new setBindRequest methods that were introduced in the 4.0.6 release. Because the server set was created with bind credentials, the pool would create a connection that tried to use those old credentials before sending a second bind request with the new credentials, and this would fail if the old credentials were no longer valid.
  • Fixed an issue with the behavior that the LDAP SDK exhibited when configured to automatically follow referrals. If the server returned a search result reference that the LDAP SDK could not follow (for example, because none of the URLs were valid, none of the servers could be reached, none of the searches succeeded, in those servers, etc.), the LDAP SDK would assign a result code of “referral” to the search operation, which would cause it to throw an exception when the search completed (as is the case for most non-success result codes). The LDAP SDK will no longer override the result code for the search operation, but will instead use whatever result code the server returned in its search result done message. Any search result references that the LDAP SDK could not automatically follow will be made available to the caller through the same mechanism that would have been used if the SDK had not been configured to automatically follow referrals (that is, either hand them off to a search result listener or collect them in a list to include in the search result object). The LDAP SDK was already making the unfollowable search result references available in this manner, but the client probably wouldn’t have gotten to the point of looking for them because of the exception resulting from the overridden operation result code.
  • Added a new LDAPConnectionPoolHealthCheck.performPoolMaintenance method that can be used to perform processing on the pool itself (rather than on any individual connection) at regular intervals as specified by the connection pool’s health check interval. This method will be invoked by the health check thread after all other periodic health checking is performed.
  • Added a new PruneUnneededConnectionsLDAPConnectionPoolHealthCheck class that can be used to monitor the size of a connection pool over time, and if the number of available (that is, not currently in use) connections is consistently greater than a specified minimum for a given length of time, then the number of connections in the pool can be reduced to that minimum. This can be used to automatically shrink the size of the pool during periods of reduced activity.
  • Updated the Schema class to provide additional constructors and methods that can be used to attempt to retrieve the schema without silently ignoring errors about unparsable elements. Previously, if a schema entry contained one or more unparsable elements, they would be silently ignored. It is now possible to more easily obtain information about unparsable elements or to have the LDAP SDK throw an exception if it encounters any unparsable elements.
  • Added createSubInitialFilter, createSubAnyFilter, and createSubFinalFilter methods to the Filter class that are more convenient to use than the existing createSubstringFilter methods for substring filters that only have one type of component.
  • Updated the Entry.diff method when operating in reversible mode so that when altering the values of an existing attribute, the delete modifications will be ordered before the add modifications. Previously, the adds came before the deletes, but this could cause problems in some directory servers, especially when the modifications are intended to change the case of a value in a case-insensitive attribute (for example, the add could be ignored or rejected because the value already exists in the entry, or the delete could end up removing the value entirely). Ordering the deletes before the adds should provide much more reliable results.
  • Updated the modrate tool to add a new “--valuePattern” argument that can be used to specify the pattern to use to generate new values. This argument is an alternative to the “--valueLength” and “--characterSet” arguments and allows for more flexibility in the types of values that can be generated.
  • Updated the manage-account tool so that the arguments related to TOTP secrets are marked sensitive. This will ensure that the value is not displayed in the clear in certain cases like interactive mode output or tool invocation logging.
  • Added a new “streamfile” value pattern component that operates like the existing “sequentialfile” component except that it limits the amount of the file that is read into memory at any given time, so it is more suitable for reading values from very large files.
  • Added a new “timestamp” value pattern component that can be used to include either the current time or a randomly selected time from a given range in a variety of formats.
  • Added a new “uuid” value pattern component that can be used to include a randomly generated universally unique identifier (UUID).
  • Added a new “random” value pattern component that can be used to include a specified number of randomly selected characters from a given character set.
  • Added a StaticUtils.toUpperCase method to complement the existing StaticUtils.toLowerCase method.
  • Added Validator.ensureNotNullOrEmpty methods that work for collections, maps, arrays, and character sequences.
  • Added LDAPTestUtils methods that can be used to make assertions about the diagnostic message of an LDAP result or an LDAP exception.
  • Added client-side support for a new exec task that can be used to invoke a specified command in the Ping Identity Directory Server (subject to security restrictions imposed by the server).
  • Added client-side support for a new file retention task that can be used to examine files in a specified directory, identify files matching a given pattern, and delete any of those files that do not match count-based, age-based, or size-based criteria.
  • Added client-side support for a new delay task that can be used sleep for a specified period of time, until the server work queue reports that all worker threads are idle and there are no pending operations, or until a given search or set of searches match at least one entry. The delay task is primarily intended to be used as a spacer between other tasks in a dependency chain.
  • Updated support for the ignore NO-USER-MODIFICATION request control to make it possible to set the criticality when creating an instance of the control. Previously, new instances were always critical.
  • Updated the ldapmodify tool to include the ignore NO-USER-MODIFICATION request control in both add and modify requests if the --ignoreNoUserModification argument was provided. Previously, that argument only caused the control to be included in add requests. Further, the control will now be marked non-critical instead of critical.
  • Updated the task API to add support for a number of new properties, including the email addresses of users to notify on task start and successful completion (in addition to the existing properties specifying users to email on error or on any type of completion), and flags indicating whether the server should alert on task start, successful completion, or failure.
  • Updated the argument parser’s properties file support so that it expects the file to use the ISO 8859-1 encoding, and to support Unicode escape sequences that are comprised of a backslash followed by the letter u and four hexadecimal digits.
  • Updated the tool invocation logger to add a failsafe mechanism for preventing passwords from being included in the log. Although it will already redact the values of any arguments that are declared sensitive, it will now also redact the values of any arguments whose name suggests that their value is a password.

UnboundID LDAP SDK for Java 4.0.6

We have just released the UnboundID LDAP SDK for Java version 4.0.6, available for download from the releases page of our GitHub repository, from the Files page of our SourceForge project, and from the Maven Central Repository. The most significant changes in this release include:

  • We fixed a number of issues in the way that the LDAP SDK handled characters whose UTF-8 representation requires more than two bytes (and therefore requires two Java chars to represent a single character). Issues related to these characters were found in code for matching rules, DNs and RDNs, and search filters.
  • We fixed an issue in the ldapsearch tool that could cause it to use an incorrect scope when constructing search requests from LDAP URLs that were read from a file.
  • We fixed a bug in schema handling that could arise if an object class definition did not explicitly specify an object class type (STRUCTURAL, AUXILIARY, or ABSTRACT). In some cases, the type could be incorrectly inherited from the superclass rather than assuming the default type of STRUCTURAL.
  • We updated the LDAPConnectionPool and LDAPThreadLocalConnectionPool classes to add new setServerSet and setBindRequest methods. These new methods make it possible to update an existing pool to change the logic that it uses for establishing and authenticating new connections.
  • We added a new LDAPRequest.setReferralConnector method that makes it possible to set a custom referral connector on a per-request basis. We also added a new RetainConnectExceptionReferralConnector class that makes it easier to obtain the exception (if any) that was caught on the last attempt to establish a connection for the purpose of following a referral.
  • Updated the in-memory directory server to better handle any java.lang.Errors that occur while interacting with a client connection. These kinds of errors should not happen under normal circumstances but may be generated by third-party code (for example, an InMemoryOperationInterceptor), and it is possible for the JVM to generate them in extraordinary circumstances like running out of memory. In such cases, the thread responsible for interacting with that client would exit without returning a response for the operation being processed and without closing the operation. The LDAP SDK will now attempt to return an error (if appropriate for the type of operation being processed) and close the connection.
  • Updated the manage-certificates tool to fix an incorrect interpretation of the path length element of a basic constraints extension.
  • Updated manage-certificates to add support for importing PEM-encoded RSA private keys that are not wrapped in a PKCS #8 envelope (that is, from a file whose header contains “BEGIN RSA PRIVATE KEY” instead of “BEGIN PRIVATE KEY”). Previously, it was only possible to import private keys using the PKCS #8 format.
  • Updated manage-certificates to add an --allow-sha-1-signature-for-issuer-certificates argument to the check-certificate-usability subcommand. If this argument is provided, then the tool will continue to call out issuer certificates whose signature is based on the now-considered-weak SHA-1 digest algorithm, but it will no longer cause the tool to exit with an error just because of that issue. This argument has no effect for certificates that use a signature based on the extremely weak MD5 digest, and it also does not have any effect if the certificate at the head of the chain (that is, the server certificate rather than the root certificate) has a SHA-1-based signature.
  • Added client-side support for a new “reload HTTP connection handler certificates” task that may be used in some Ping Identity server products to request that the server dynamically reload the certificate key and trust stores used by all HTTP connection handler instances that provide support for HTTPS.

CVE-2018-1000134 and the UnboundID LDAP SDK for Java

On Friday, March 16, 2018, CVE-2018-1000134 was published, describing a vulnerability in the UnboundID LDAP SDK for Java. The vulnerability has been fixed in LDAP SDK version 4.0.5, which is available for immediate download from the LDAP.com website, from the releases page of our GitHub repository, from the Files page of our SourceForge project, and from the Maven Central Repository.

This post will explain the issue in detail (see the release notes for information about other changes in LDAP SDK version 4.0.5). However, to quickly determine whether your application is vulnerable, you should check to see if all of the following conditions are true:

  • You are using the LDAP SDK in synchronous mode. Although this mode is recommended for applications that do not require asynchronous functionality, the LDAP SDK does not use this mode by default.
  • You use the LDAP SDK to perform simple bind operations for the purpose of authenticating users to a directory server. This is a very common use case for LDAP-enabled applications.
  • Your application does not attempt to verify whether the user actually provided a password. This is unfortunately all too common for LDAP-enabled applications.
  • The simple bind requests are sent to a directory server that does not follow the RFC 4513 section 5.1.2 recommendation to reject simple bind requests with a non-empty DN and an empty password. Although this recommendation is part of the revised LDAPv3 specification published in 2006, there are apparently some directory servers that still do not follow this recommendation by default.

If your application meets all of these criteria, then you should take action immediately to protect yourself. The simplest way to fix the vulnerability in your application is to update it to use the 4.0.5 release of the LDAP SDK. However, you should also ensure that your applications properly validate all user input, and it may also be a good idea to consider switching to a more modern directory server.

The Vulnerability in LDAPv3

The original LDAPv3 protocol specification was published as RFC 2251 in December 1997. LDAPv3 is a very impressive protocol in most regards, but perhaps the most glaring problem in the specification lies in the following paragraph in section 4.2.2:

If no authentication is to be performed, then the simple authentication option MUST be chosen, and the password be of zero length. (This is often done by LDAPv2 clients.) Typically the DN is also of zero length.

It’s that word “typically” in this last sentence that has been the source of a great many vulnerabilities in LDAP-enabled applications. Usually, when you want to perform an anonymous simple bind, you provide an empty string for both the DN and the password. However, according to the letter of the specification above, you don’t have to provide an empty DN. As long as the password is empty, the server will treat it as an anonymous simple bind.

In applications that use an LDAP simple bind to authenticate users, it’s a very common practice to provide two fields on the login form: one for the username (or email address or phone number or some other kind of identifier), and one for the password. The application first performs a search to see if they can map that username to exactly one user in the directory, and if so, then it performs a simple bind with the DN of that user’s entry and the provided password. As long as that the server returns a “success” response to the bind request, then the application considers the user authenticated and will grant them whatever access that user is supposed to have.

However, a problem can arise if the application just blindly takes whatever password was provided in the login form and plugs it into the simple bind request without actually checking to see whether the user provided any password at all. In such cases, if the user provided a valid username but an empty password, then the application will perform a simple bind request with a valid DN but no password. The directory server will interpret that as an anonymous simple bind and will return a success result, and the application will assume that the user is authenticated even though they didn’t actually provide any password at all.

This is such a big problem in LDAP-enabled applications that it was specifically addressed in the updated LDAPv3 specification published in June 2006. RFC 4513 section 5.1.2 states the following:

Unauthenticated Bind operations can have significant security issues (see Section 6.3.1). In particular, users intending to perform Name/Password authentication may inadvertently provide an empty password and thus cause poorly implemented clients to request Unauthenticated access. Clients SHOULD be implemented to require user selection of the Unauthenticated Authentication Mechanism by means other than user input of an empty password. Clients SHOULD disallow an empty password input to a Name/Password Authentication user interface. Additionally, Servers SHOULD by default fail Unauthenticated Bind requests with a resultCode of unwillingToPerform.

Further, section 6.3.1 of the same RFC states:

Operational experience shows that clients can (and frequently do) misuse the unauthenticated access mechanism of the simple Bind method (see Section 5.1.2). For example, a client program might make a decision to grant access to non-directory information on the basis of successfully completing a Bind operation. LDAP server implementations may return a success response to an unauthenticated Bind request. This may erroneously leave the client with the impression that the server has successfully authenticated the identity represented by the distinguished name when in reality, an anonymous authorization state has been established. Clients that use the results from a simple Bind operation to make authorization decisions should actively detect unauthenticated Bind requests (by verifying that the supplied password is not empty) and react appropriately.

In directory servers that follow the recommendation from RFC 4513 section 5.1.2, clients can perform an anonymous simple bind by providing an empty DN and an empty password, but an attempt to bind with a non-empty DN and an empty password will be rejected. This very good recommendation was made over ten years ago, and the code change needed to implement it is probably very simple. However, for some reason, there are directory server implementations out there that haven’t been updated to follow this recommendation, and therefore leave client applications open to this inadvertent vulnerability.

The Vulnerability in the UnboundID LDAP SDK for Java

Ever since its initial release, the UnboundID LDAP SDK for Java has attempted to protect against simple bind requests that include a non-empty DN with an empty password. The LDAPConnectionOptions class provides a setBindWithDNRequiresPassword(boolean) method that you can use to indicate whether the LDAP SDK will reject a simple bind request that has a non-empty DN with an empty password. If you don’t explicitly use this option, then the LDAP SDK will assume a default value of true. If you try to send a simple bind request that includes a non-empty DN and an empty password, then the LDAP SDK won’t actually send any request to the server but will instead throw an LDAPException with a result code of ResultCode.PARAM_ERROR and a message of “Simple bind operations are not allowed to contain a bind DN without a password.”

Or at least, that’s the intended behavior. And that is the behavior that you’ll get if you send the bind request in the asynchronous mode that the LDAP SDK uses by default. However, Stanis Shkel created GitHub issue #40 (“processSync in SimpleBindRequest allows empty password with set bindDN”), which points out that this check was skipped for connections operating in synchronous mode.

LDAP is an asynchronous protocol. With a few exceptions, it’s possible to have multiple operations in progress simultaneously over the same LDAP connection. To support that asynchronous capability, the LDAP SDK maintains an extra background thread that constantly read data from a connection and makes sure that any data sent from the server gets delivered to whichever thread is waiting for it. This is just fine most of the time, but it does come at the cost of increased resource consumption, and a small performance hit from handing off data from one thread to another. To minimize this impact for applications that don’t take advantage of the asynchronous capabilities that LDAP provides, we added a synchronous mode to the LDAP SDK way back in version 0.9.10 (released in July of 2009). In this mode, the same thread that sends a request to the server is the one that waits for and reads the response. This can provide better performance and lower resource consumption, but you have to explicitly enable it using the LDAPConnectionOptions.setUseSynchronousMode(boolean) method before establishing a connection.

In the course of implementing support for the synchronous mode for a simple bind request, we incorrectly put the check for synchronous mode before the check for an empty password. For a connection operating in synchronous mode, we branched off to another part of the code and skipped the check for an empty password. The fix for the problem was simple: move the check for an empty password above the check for synchronous mode, and it was committed about three and a half hours after the issue was reported, including a unit test to ensure that a simple bind request with a non-empty DN and an empty password is properly rejected when operating in synchronous mode (there was already a test to ensure the correct behavior in the default asynchronous mode).

Conditions Necessary for the Vulnerability

Although there was unquestionably a bug in the LDAP SDK that created the possibility for this bug, there are a number of factors that could have prevented an application from being susceptible to it. Only an application that meets all of the following conditions would have been vulnerable:

  • The application must have explicitly enabled the use of synchronous mode when creating an LDAP connection or connection pool. If the application was using the default asynchronous mode, it would not have been vulnerable.
  • The application must have created simple bind requests from untrusted and unverified user input. If the application did not create simple bind requests (for example, because it did not perform binds at all, or because it used SASL authentication instead of simple), then it would not have been vulnerable. Alternately, if the application validated the user input to ensure that it would not attempt to bind with an empty password, then it would not have been vulnerable.
  • The application must have sent the simple bind request to a server that does not follow the RFC 4513 recommendations. If the server is configured to reject simple bind requests that contain a non-empty DN with an empty password, then an application communicating with that server would not have been vulnerable.

While we strongly recommend updating to LDAP SDK version 4.0.5, which no longer has the bug described in CVE-2018-1000134, we also strongly recommend ensuring that applications properly validate all user input as additional mitigation against problems like this. And if you’re using a directory server that hasn’t been updated to apply a very simple update to avoid a problem that has been well known and clearly documented for well over a decade, then perhaps you should consider updating to a directory server that takes security and standards compliance more seriously.

UnboundID LDAP SDK for Java 4.0.4

We have just released the UnboundID LDAP SDK for Java version 4.0.4, available for download from the LDAP.com website, from the releases page of our GitHub repository, from the Files page of our SourceForge project, and from the Maven Central Repository.

There are a few noteworthy changes included in this release. The release notes go into more detail, but the highlights of these changes include:

  • We updated the way that the LDAP SDK generates exception messages to make them more user-friendly. They are now less likely to include stack traces, and they are less likely to include repeated information (like LDAP SDK build information, and information duplicated from an exception’s cause).
  • We fixed an issue that could cause multiple application threads to block in the course of closing a connection pool.
  • We updated the way that the LDAP SDK sends LDAP messages so that it is more resilient to stalls in the TLS negotiation process.
  • We updated the LDAP SDK’s ServerSet implementations so that they can perform authentication and post-connect processing, which can make health checks against newly established connections more reliable.
  • We updated the GetEntryLDAPConnectionPoolHealthCheck class to provide support for invoking the health check after a pooled connection has been authenticated.
  • We fixed a bug in the GetEntryLDAPConnectionPoolHealthCheck class that could cause it to behave incorrectly when checking the validity of a connection after an LDAPException was caught.
  • We updated the Attribute.hasValue method to be more efficient for attributes with multiple values, and especially for attributes with a lot of values or with more complicated matching rules. This will also improve the Filter.matchesEntry method for equality filters that target similar types of attributes.
  • We updated the prompt trust manager to provide better output formatting, and to provide additional warnings about conditions that may make a server certificate chain less trustworthy.
  • We updated the LDAPConnectionOptions class to adjust the initial default connect timeout and operation response timeout, and the default operation response timeout can now be set differently for each type of operation. Most of the default values for options in the LDAPConnectionOptions class can now be set via system properties.