Table of Contents
- Introduction
- Establishing a Connection
- Searching Hierarchical Data (this page)
- Browsing Attributes
- Timeouts
- Modifying Data
Searching Hierarchical Data
Once you’ve established a connection, the next thing you’ll probably want to do is start searching for records. Either to pull the data back to a client, or to modify one or more records or simply to check for existence of some records. Such as authorization checks based on a user’s membership in one or more groups; or searching for users with particular attributes.
One of the important things to remember about LDAP searches is, unlike the flat nature of a SQL WHERE-clause against a table, the data is hierarchical. That is, you must specify a depth scope when looking at data.
So, using the sample hierarchy from the introduction:
DC=NET DC=Example OU=People OU=Managers uid=1234 CN=Jane Doe hiredate=2005-04-03 email=jane.doe@example.net department=Acquisitions objectClass=person uid=3456 CN=John Doe hiredate=2006-05-04 email=john.doe@example.net department=Sales objectClass=person OU=Consultants uid=1987 CN=Mickey Mouse hiredate=2005-12-11 email=mickey.mouse@example.net department=Acquisitions objectClass=person uid=6543 CN=Donald Duck hiredate=2008-07-06 email=donald.duck@example.net department=Sales objectClass=person
-
We could search for the existence of a particular record directly.
uid=1987,ou=consultants,ou=people,dc=example,dc=net
-
We could search one level, perhaps getting a list of all consultants.
ou=consultants,ou=people,dc=example,dc=net
-
We can search an entire subtree, for example, all people regardless of group.
ou=people,dc=example,dc=net
Within DBMS_LDAP the three search scopes are defined by constants:
SCOPE_BASE CONSTANT NUMBER := 0; SCOPE_ONELEVEL CONSTANT NUMBER := 1; SCOPE_SUBTREE CONSTANT NUMBER := 2;
While some LDAP servers and clients support a 4th type subordinate sub-tree searching; DBMS_LDAP does not provide such a mechanism. You could mimic the functionality (but not the efficiency) with a sub-tree search combined with an appropriate filter to eliminate the top level group from the results.
In addition to the search scope, you must also specify what will be returned from a search that matches the criteria. There are three levels of detail in the results:
- Entry Distinguished Names only
- Entry Distinguished Names, along with names of Attributes for each entry
- Entry Distinguished Names, along with the names and values of Attributes for each entry
Attributes are specified by a string collection regardless of the level of detail. You can provide a string collection populated with the names of the attributes to be returned for each entry. Alternately, define the collection to have only one entry with a wildcard “*” to get all attributes, or provide a collection of one entry consisting of the magic string “1.1”, meaning return no attributes at all.
Within this article, all searches will use “1.1”, hence excluding attributes from the results. Including the attributes and browsing their names and values will be explored in the next chapter of this series.
Searching with BASE scope, no attributes
The simplest search is just a BASE scope, meaning you will provide the entire DN as a location. If you include no attributes, then this type of search is mosty just a an existence check. If you can find the record it exists, if you can’t, it doesn’t.
In the example below we’ll search the public Debian LDAP server for a specific host name. To use a BASE scope, we use the constant DBMS_LDAP.SCOPE_BASE. We’ll return no attributes, so our search list is “1.1”. You must specify a filter on the search, in this case we’ll leave it open to any record that matches the specified DN. Every record has an objectclass attribute, so our filter is a wildcard on that attribute: “objectclass=*”. The attronly parameter doesn’t apply since we aren’t returning any attributes so just leave it NULL.
In these examples, if we find a match we’ll display the DN as a single string and also break the DN into its constituent levels within the hierarchy.
If we don’t find a matching entry then the search_s function will raise an exception.
DECLARE v_result PLS_INTEGER; v_session DBMS_LDAP.session; v_search_attrs DBMS_LDAP.string_collection; v_search_results DBMS_LDAP.MESSAGE; v_entry DBMS_LDAP.MESSAGE; v_distinguished_name VARCHAR2(256); v_dn_pieces DBMS_LDAP.string_collection; BEGIN DBMS_LDAP.use_exception := TRUE; v_session := DBMS_LDAP.init(hostname => 'db.debian.org', portnum => DBMS_LDAP.port); v_search_attrs(1) := '1.1'; v_result := DBMS_LDAP.search_s(ld => v_session, base => 'host=beach,ou=hosts,dc=debian,dc=org', scope => DBMS_LDAP.scope_base, filter => 'objectclass=*', attrs => v_search_attrs, attronly => NULL, res => v_search_results); v_entry := DBMS_LDAP.first_entry(v_session, v_search_results); v_distinguished_name := DBMS_LDAP.get_dn(v_session, v_entry); DBMS_OUTPUT.put_line('DN: ' || v_distinguished_name); v_dn_pieces := DBMS_LDAP.explode_dn(dn => v_distinguished_name, notypes => 0); FOR i IN v_dn_pieces.FIRST .. v_dn_pieces.LAST LOOP DBMS_OUTPUT.put_line(i || ': ' || v_dn_pieces(i)); END LOOP; v_result := DBMS_LDAP.msgfree(v_entry); v_result := DBMS_LDAP.unbind_s(v_session); END; / DN: host=beach,ou=hosts,dc=debian,dc=org 0: host=beach 1: ou=hosts 2: dc=debian 3: dc=org
Instead of raising an exception we could disable exceptions for the search and then check the result value of the search_s function. You can test this by changing “beach” in the sample to any host name not in their server, like “xxxxxxx”.
DECLARE v_result PLS_INTEGER; v_session DBMS_LDAP.session; v_search_attrs DBMS_LDAP.string_collection; v_search_results DBMS_LDAP.MESSAGE; v_entry DBMS_LDAP.MESSAGE; v_distinguished_name VARCHAR2(256); v_dn_pieces DBMS_LDAP.string_collection; BEGIN DBMS_LDAP.use_exception := TRUE; v_session := DBMS_LDAP.init(hostname => 'db.debian.org', portnum => DBMS_LDAP.port); v_search_attrs(1) := '1.1'; DBMS_LDAP.use_exception := FALSE; v_result := DBMS_LDAP.search_s(ld => v_session, base => 'host=xxxxxxx,ou=hosts,dc=debian,dc=org', scope => DBMS_LDAP.scope_base, filter => 'objectclass=*', attrs => v_search_attrs, attronly => NULL, res => v_search_results); IF v_result = DBMS_LDAP.no_such_object THEN DBMS_OUTPUT.put_line('Base DN not found'); ELSE DBMS_LDAP.use_exception := TRUE; v_entry := DBMS_LDAP.first_entry(v_session, v_search_results); v_distinguished_name := DBMS_LDAP.get_dn(v_session, v_entry); DBMS_OUTPUT.put_line('DN: ' || v_distinguished_name); v_dn_pieces := DBMS_LDAP.explode_dn(dn => v_distinguished_name, notypes => 0); FOR i IN v_dn_pieces.FIRST .. v_dn_pieces.LAST LOOP DBMS_OUTPUT.put_line(i || ': ' || v_dn_pieces(i)); END LOOP; v_result := DBMS_LDAP.msgfree(v_entry); END IF; v_result := DBMS_LDAP.unbind_s(v_session); END; / Base DN not found
For the remainder of the examples I will simply raise exceptions on a failed search.
Searching with scope of One Sub-Level, no attributes
Instead of searching for a record at a specific DN, you could instead search for any entries within one level of the directory matching your search criteria. Here we’ll again search within the hosts level, but we’ll allow all records under hosts. As the name implies, the constant to do one-level searching is DBMS_LDAP.SCOPE_ONELEVEL.
DECLARE v_result PLS_INTEGER; v_session DBMS_LDAP.session; v_search_attrs DBMS_LDAP.string_collection; v_search_results DBMS_LDAP.MESSAGE; v_entry DBMS_LDAP.MESSAGE; v_distinguished_name VARCHAR2(256); BEGIN DBMS_LDAP.use_exception := TRUE; v_session := DBMS_LDAP.init(hostname => 'db.debian.org', portnum => DBMS_LDAP.port); v_search_attrs(1) := '1.1'; v_result := DBMS_LDAP.search_s(ld => v_session, base => 'ou=hosts,dc=debian,dc=org', scope => DBMS_LDAP.scope_onelevel, filter => 'objectclass=*', attrs => v_search_attrs, attronly => 1, res => v_search_results); v_result := DBMS_LDAP.count_entries(v_session, v_search_results); DBMS_OUTPUT.put_line('Entry count: ' || v_result); v_entry := DBMS_LDAP.first_entry(v_session, v_search_results); WHILE v_entry IS NOT NULL LOOP v_distinguished_name := DBMS_LDAP.get_dn(v_session, v_entry); DBMS_OUTPUT.put_line('DN: ' || v_distinguished_name); v_entry := DBMS_LDAP.next_entry(v_session, v_entry); END LOOP; v_result := DBMS_LDAP.unbind_s(v_session); END; / Entry count: 192 DN: host=klecker,ou=hosts,dc=debian,dc=org DN: host=lobos,ou=hosts,dc=debian,dc=org DN: host=wieck,ou=hosts,dc=debian,dc=org DN: host=schumann,ou=hosts,dc=debian,dc=org DN: host=handel,ou=hosts,dc=debian,dc=org DN: host=geo1,ou=hosts,dc=debian,dc=org DN: host=geo2,ou=hosts,dc=debian,dc=org DN: host=draghi,ou=hosts,dc=debian,dc=org DN: host=kaufmann,ou=hosts,dc=debian,dc=org DN: host=wolkenstein,ou=hosts,dc=debian,dc=org DN: host=sibelius,ou=hosts,dc=debian,dc=org DN: host=tchaikovsky,ou=hosts,dc=debian,dc=org DN: host=pettersson,ou=hosts,dc=debian,dc=org DN: host=lully,ou=hosts,dc=debian,dc=org DN: host=abel,ou=hosts,dc=debian,dc=org DN: host=arnold,ou=hosts,dc=debian,dc=org DN: host=antheil,ou=hosts,dc=debian,dc=org ...
Searching with scope of entire Sub-Tree, no attributes
And finally the last scope to explore is the Sub-Tree scope which will search for records at any and all depths within a directory tree below the level of the specified base DN. Here again the package includes a constant: DBMS_LDAP.SCOPE_SUBTREE. Since we’re searching the entire tree the results could get large, especially if our filter allows every object class to be returned.
So, in this example, we’ll restrict the filter to object classes of type “debiangroup”. We’ll start the search at the top of the tree looking for everything under “dc=debian,dc=org”.
DECLARE v_result PLS_INTEGER; v_session DBMS_LDAP.session; v_search_attrs DBMS_LDAP.string_collection; v_search_results DBMS_LDAP.MESSAGE; v_entry DBMS_LDAP.MESSAGE; v_distinguished_name VARCHAR2(256); BEGIN DBMS_LDAP.use_exception := TRUE; v_session := DBMS_LDAP.init(hostname => 'db.debian.org', portnum => DBMS_LDAP.port); v_search_attrs(1) := '1.1'; v_result := DBMS_LDAP.search_s(ld => v_session, base => 'dc=debian,dc=org', scope => DBMS_LDAP.scope_subtree, filter => 'objectclass=debiangroup', attrs => v_search_attrs, attronly => 1, res => v_search_results); v_result := DBMS_LDAP.count_entries(v_session, v_search_results); DBMS_OUTPUT.put_line('Entry count: ' || v_result); v_entry := DBMS_LDAP.first_entry(v_session, v_search_results); WHILE v_entry IS NOT NULL LOOP v_distinguished_name := DBMS_LDAP.get_dn(v_session, v_entry); DBMS_OUTPUT.put_line('DN: ' || v_distinguished_name); v_entry := DBMS_LDAP.next_entry(v_session, v_entry); END LOOP; v_result := DBMS_LDAP.unbind_s(v_session); END; / Entry count: 2172 DN: gid=nm,ou=users,dc=debian,dc=org DN: gid=qa,ou=users,dc=debian,dc=org DN: gid=adm,ou=users,dc=debian,dc=org DN: gid=wnpp,ou=users,dc=debian,dc=org DN: gid=list,ou=users,dc=debian,dc=org DN: gid=dput_OBS,ou=users,dc=debian,dc=org DN: gid=visi,ou=users,dc=debian,dc=org DN: gid=above,ou=users,dc=debian,dc=org DN: gid=udcvs,ou=users,dc=debian,dc=org DN: gid=guest,ou=users,dc=debian,dc=org DN: gid=debwww,ou=users,dc=debian,dc=org DN: gid=doccvs,ou=users,dc=debian,dc=org DN: gid=globus,ou=users,dc=debian,dc=org DN: gid=debcvs,ou=users,dc=debian,dc=org DN: gid=popcon,ou=users,dc=debian,dc=org DN: gid=webwml,ou=users,dc=debian,dc=org DN: gid=wb-ppc,ou=users,dc=debian,dc=org DN: gid=buildd,ou=users,dc=debian,dc=org DN: gid=aptcvs,ou=users,dc=debian,dc=org DN: gid=Debian,ou=users,dc=debian,dc=org DN: gid=wb-arm,ou=users,dc=debian,dc=org ...
Searching with Filters
The Search API requires a filter, so each of the examples above includes one but all were fairly simple. Either the generic all filter of ‘objectclass=*’
or a specific object class ‘objectclass=”debian group”‘. The LDAP filter syntax is much richer though. The parameter is simply a VARCHAR2, and the filter string is passed “as is” to the server, so the supported functionality is determined by the server and not the DBMS_LDAP package.
A basic LDAP filter takes the form of:
(atttribute_name operator value)
- The attribute name is the name of any attribute that might be found within an entry. It need not be an attribute that exists in every entry, as many(most) will not.
- The operator could be =, >=, or <=. Inequality comparisons can be a little tricky. From the client side, all of the data appears as text; but within the LDAP server the data is managed by a schema with types and defined comparison rules. Those rules may use case-sensitive text comparison, or case-insensitive. Numerals may be compared as numeric or text data depending on the schema's definition for that attribute. Some data may not have ordering rules making less-than or greater-than comparisons meaningless. An attribute classifying an entry as "animal", "vegetable", or "mineral" for example would not necessarily have an ordering criteria, only an equality or inequality comparison.
- The filter value could be a string like the “debian group” example earlier, or a wild card like “*”, or a combination of string and wildcard such as “abc*” to find all entries with an attribute beginning with “abc”.
- Technically, a filter should be wrapped in parentheses; but if you have a single filter condition with no modifiers, then DBMS_LDAP will accept it as is.
- The AND operator “&” applies the intersection of conditions that follow.
(&(a=1)(b=2)) – Find records where a=1 and b=2, i.e. (a,b) = (1,2)
(&(x=10)(y=100)(z=1000)) – Find records where x=10 and y=100 and z=1000, i.e. (x,y,z) = (10,100,1000)
As a matter of efficiency, it is usually better to have an AND condition with many sub-filters than a more extensive filter using many AND conditions with fewer filters in each.It is legal to have a filter consisting of only the AND operator: (&). This filter condition simply evaluates to TRUE for any entry.
- The OR operator “|” applies the union of the conditions that follow.
(|(a=1)(b=2)) – Find any records where a=1 or b=2
(|(x=10)(y=100)(z=1000)) – Find records where x=10 or y=100 or z=1000
Similar to AND operators, it is usually better to have an OR condition with many sub-filters than to use many OR conditions across fewer sub-filters.It is legal to have a filter consisting of only the OR operator: (|). This filter condition simply evaluates to FALSE for any entry.
- The NEGATION operator “!” simply negates whatever condition follows it.
(!(objectClass=server)) – Find all records that do not have a server object class
Unlike the AND and OR operators, the NEGATION operator can not operate on a list of conditions, only a single filter condition can be negated with a single NEGATION operator.
The basic filters can then be modified by AND, OR, or NEGATION operators.
And finally, you can nest filter conditions within one another, thus allowing more complicated conditions such as: Find all Managers named Jane Doe or Consultants named Mickey Mouse.
(|(&(OU=Managers)(CN=Jane Doe))(&(OU=Consultants)(CN=Mickey Mouse)))
With some formatting, the logic is a little easier to follow.
(| (&(OU=Managers)(CN=Jane Doe)) (&(OU=Consultants)(CN=Mickey Mouse)) )
Or, we could find John Doe and Donald Duck by negating the previous search.
(! (| (&(OU=Managers)(CN=Jane Doe)) (&(OU=Consultants)(CN=Mickey Mouse)) ) )
Data size limits and result set pagination
There is one additional filtering criteria you may run into with large directories. That is a size limit on returned results. LDAP servers may be willing to pump an unlimited amount of data back to a client, or may have the limits set high enough that your searches never exceed their thresholds; but eventually you will likely come across a result set too large to be returned. When that happens you will get an exception:
ORA-31202: DBMS_LDAP: LDAP client/server error: Sizelimit exceeded
or, if you have exceptions turned off, then search_s will return a result code of DBMS_LDAP.SIZELIMIT_EXCEEDED.
While many LDAP servers support paginated results to handle this situation, DBMS_LDAP, unfortunately, does not have a means of invoking such functionality.
If you encounter this the only way to get around it from the client side is to construct a series of filters that will span the entire data set.
In the example below I use the following filter:
filter => '&(status=Active)(uid=ab*)'
Simply searching for all accounts failed. Searching for all Active accounts still failed. Searching for all Active accounts with a uid starting with an “a” still failed. When I got to the point of limiting the uid to those starting with “ab” for just Active accounts I finally got small enough results to pass the server’s size limit. I could then iterate across aa-zz combinations for both Active and not Active accounts to pull all of the data. Of course I might miss ids with leading numeric characters; but hopefully, if you had to do manual pagination you would work with the server’s admins to determine what the best spanning ranges would be.
DECLARE v_result PLS_INTEGER; v_session DBMS_LDAP.session; v_search_attrs DBMS_LDAP.string_collection; v_search_results DBMS_LDAP.MESSAGE; v_entry DBMS_LDAP.MESSAGE; v_distinguished_name VARCHAR2(256); v_dn_pieces DBMS_LDAP.string_collection; BEGIN DBMS_LDAP.use_exception := TRUE; v_session := DBMS_LDAP.init(hostname => 'ldap.andrew.cmu.edu', portnum => DBMS_LDAP.port); v_search_attrs(1) := '1.1'; DBMS_LDAP.use_exception := FALSE; v_result := DBMS_LDAP.search_s(ld => v_session, base => 'ou=account,dc=cmu,dc=edu', scope => DBMS_LDAP.scope_onelevel, filter => '&(status=Active)(uid=ab*)', attrs => v_search_attrs, attronly => 1, res => v_search_results); IF v_result != DBMS_LDAP.success THEN DBMS_OUTPUT.put_line('Search failed with code: ' || v_result || ' ' || DBMS_LDAP.err2string(v_result)); ELSE DBMS_LDAP.use_exception := FALSE; v_result := DBMS_LDAP.count_entries(v_session, v_search_results); DBMS_OUTPUT.put_line('Entry count: ' || v_result); v_entry := DBMS_LDAP.first_entry(v_session, v_search_results); WHILE v_entry IS NOT NULL LOOP v_distinguished_name := DBMS_LDAP.get_dn(v_session, v_entry); DBMS_OUTPUT.put_line('DN: ' || v_distinguished_name); v_entry := DBMS_LDAP.next_entry(v_session, v_entry); END LOOP; END IF; v_result := DBMS_LDAP.unbind_s(v_session); END; / Entry count: 71 DN: uid=ABHAT,ou=account,dc=cmu,dc=edu DN: uid=ABRUNK,ou=account,dc=cmu,dc=edu DN: uid=ABIHAUS,ou=account,dc=cmu,dc=edu DN: uid=ABUSE,ou=account,dc=cmu,dc=edu DN: uid=ABOY,ou=account,dc=cmu,dc=edu DN: uid=ABERENDEYEV,ou=account,dc=cmu,dc=edu DN: uid=abenavides,ou=account,dc=cmu,dc=edu DN: uid=abelsaj,ou=account,dc=cmu,dc=edu DN: uid=abhinav,ou=account,dc=cmu,dc=edu DN: uid=abeer,ou=account,dc=cmu,dc=edu DN: uid=abhishekveldurthy,ou=account,dc=cmu,dc=edu DN: uid=ab,ou=account,dc=cmu,dc=edu DN: uid=abhay,ou=account,dc=cmu,dc=edu DN: uid=abinajay,ou=account,dc=cmu,dc=edu DN: uid=abhayjoseph,ou=account,dc=cmu,dc=edu DN: uid=abhishekjain,ou=account,dc=cmu,dc=edu DN: uid=abreis,ou=account,dc=cmu,dc=edu DN: uid=abhinavas,ou=account,dc=cmu,dc=edu ...
That covers all of the basic of searching for entries within an LDAP hierarchy.
In the next section we’ll extend the search results to include attribute lists with and without their values.
Hi Sean, your reference is so much preciuos. if I try a SUBTREE scope search with objectclass=* i find that search is bounded to solely the OracleContext formerly created in order to configure ldap (AD) database services name resolution. Is workaround this limitation possible?
Thanks and regards
Flavio
Hello, I’m glad you found it useful. If you’re looking under the OracleContext branch, the only layer under that subtree is the service name entries and aliases. so, if you’re getting that, the subtree is working.
If I’m misunderstanding what you’re saying, could you post a code sample with the results you’re getting compared to the results you’re expecting?
We have a Microsoft Active Directory service and a group within an OU that can have say 5000 members associated with a single group. Because it exceeds 1500 ( a hard limit on search ) we do not get SIZELIMIT_EXCEEDED, we simple get 0 attributes returned because DBMS_LDAP cannot fetch the attributes when page range number are returned (member;range=0-1499).
Do you have a method to cater for this using DBMS_LDAP?
Since DBMS_LDAP doesn’t have support for pagination or client-side fetch limits there aren’t really any good options. The closest thing I can think of is to loop with filters that will span the entire data set for you but in “chunks” small enough to fit in your server’s limits. It’s especially awkward if you aren’t getting the standard error code returned to you. Maybe you could ask your admin if that could be turned on to give you better feed back? Without it you’ll have to just kind of guess.
It’s very interesting, thanks for those examples. Can you Help me ?. I am trying fetch with DBMS_LDAP a set of user for specified attribute, example : dateCreated or lastModified in order to load in a RDBMS. Do you have some solution for this problem??. My OID has one million users.
The last code block has an example of searching with attribute filters.
I would imagine your filter would look something like this:
filter => ‘|(dateCreated=YOUR_DATE_VALUE)(lastModified=YOUR_DATE_VALUE)’