Treequel Logo

Treequel

Version: 1.0.0

This is a manual for Treequel, a Ruby library that is intended to make interacting with an LDAP directory natural and easy without trying to make it behave like a relational database. It’s built on top of Ruby-LDAP, so if you don’t already have that installed you’ll need to install it (gem install ruby-ldap should work for modern versions of Ruby), and you’ll need to have access to an LDAP server, of course.

Connecting to a Directory

Once those things are done, you can fire Treequel up via IRb and get a Treequel::Directory object to play around with:

$ irb -rtreequel -rubygems
irb> dir = Treequel.directory
# => #<Treequel::Directory:0x69cbac localhost:389 (not connected) base="dc=acme,dc=com", bound as=anonymous, schema=(schema not loaded)>
Fetching a directory.

The .directory method has some reasonable defaults, so if your directory is running on localhost, you want to connect using TLS on the default port, and bind anonymously, this will be all you need.

For anything other than testing, though, it’s likely you’ll want to control the connection parameters a bit more than that. There are two options for doing this: via an LDAP URL, or with a Hash of options.

Connecting With an LDAP URL

The LDAP URL format can contain quite a lot of information, but Treequel::Directory only uses the scheme, host, port, and base DN parts:

irb> dir = Treequel.directory( 'ldap://ldap.andrew.cmu.edu/dc=cmu,dc=edu' )
# => #<Treequel::Directory:0x4f052e ldap.andrew.cmu.edu:389 (not connected) base="dc=cmu,dc=edu", bound as=anonymous, schema=(schema not loaded)>
Fetching a directory by URL.

You may omit the base DN in the URL if your environment only has one top level base (or you don’t know it!) Treequel will use the first base DN it finds from the server’s advertised namingContexts by default.

It will also use the user and password from a user:pass@host-style URL, if present, and use them to immediately bind to the directory. See the section on binding for details.

Connecting With an Options Hash

Creating a directory with an options hash allows more fine-grained control over the connection and binding parameters. It’s the same as the hash supported by Treequel::Directory’s constructor:

:host
The LDAP host to connect to.
:port
The port to connect to.
:connect_type
The type of connection to establish. Must be one of :plain, :tls, or :ssl.
:base_dn
The base DN of the directory.
:bind_dn
The DN of the user to bind as.
:pass
The password to use when binding.

Any values which you don’t provide will default to the values in Treequel::Directory::DEFAULT_OPTIONS.

irb> dir = Treequel.directory( :host => 'localhost', :base_dn => 'dc=acme,dc=com' )
# => => #<Treequel::Directory:0x4f2586 localhost:389 (not connected) base="dc=acme,dc=com",    bound as=anonymous, schema=(schema not loaded)>
Fetching a directory with an options hash.

Connecting with a URL and an Options Hash

You can also mix the two connection styles, allowing you to still use a compact URL, but set the connection_type explicitly, e.g.:

irb> dir = Treequel.directory( 'ldap://localhost/dc=acme,dc=com', :connect_type => :plain )
# => #<Treequel::Directory:0x4a0844 localhost:389 (not connected) base="dc=acme,dc=com",    bound as=anonymous, schema=(schema not loaded)>
Using both a URL and options.

Binding to the Directory

If you don’t specify a user DN and password, a new Directory object will be bound anonymously, which is usually sufficient for reading the public attributes of records, but it’s likely that you’ll need to bind as a particular user to write to the directory or access protected attributes:

irb> dir.bind( 'uid=mgranger,ou=people,dc=acme,dc=com', 'my_password' )
# => "uid=mgranger,ou=people,dc=laika,dc=com"
Binding to the directory.

You can also bind to the directory by creating it using a URL that contains authority information; this is not recommended for production use, as it requires that the password be in plain text in the connection information, but it’s supported for convenience’s sake:

irb> url = 'ldap://cn=user,dc=acme,dc=com:my_password@localhost/dc=acme,dc=com'
irb> dir = Treequel.directory( url )
# => #<Treequel::Directory:0x4f052e localhost:389 (not connected) base="dc=acme,dc=com", bound as="cn=user,dc=acme,dc=com", schema=(schema not loaded)>
Fetching a directory by URL with automatic binding.

Unbinding

You can also revert back to an anonymous binding by calling #unbind.

Binding With A Block

If you want to rebind as a different user for just a few operations, you can do that by calling the #bound_as method with a block that contains the operations which require more privileges:

irb> dir.bound_as( 'cn=admin,dc=acme,dc=com', 's00per:sekrit' ) { dir }
# => #<Treequel::Directory:0x4f052e localhost:389 (not connected) base="dc=acme,dc=com", bound as="cn=admin,dc=acme,dc=com", schema=(schema not loaded)>
Executing a block with a different binding.

Once the block returns, the binding reverts to what it previously was.

There are a bunch of other things you can do with the Directory object, but in most cases you won’t interact with it directly except as the root of the directory. To interact with the entries in the directory, you’ll probably want to start with a Treequel::Branch

Branches

Once you’ve established a connection to a directory, you can fetch entries from the directory hierarchy by traversing the directory hierarchy using Branches. A Branch (Treequel::Branch) is just a wrapper around a DN. The wrapped DN doesn’t necessarily need to map to an extant entry in the directory; the entry behind it isn’t fetched until it’s needed for something.

You can get a Branch for a DN in several ways. The easiest, once you have a Directory, is to use the RDN from the base of the directory to fetch it. You can fetch a Branch from a Directory or any other Branch by calling a method on it with the same name as one of the attributes of the RDN you want to traverse, and passing the value as the first argument to that method.

For instance, my company’s directory has people organized under a top-level OU called “people”, so I can fetch a Branch for it like so:

irb> people = dir.ou( :people )
# => #<Treequel::Branch:0x19a76d4 ou=people,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry=nil>
irb> people.dn
# => "ou=people,dc=acme,dc=com"
Fetching a branch ou=people.

Then you can fetch branches for individuals under ou=People by calling their RDN method, too. Since I happen to know that all of my company’s People are keyed by uid, everyone’s RDN from ou=People will be uid=«something»:

irb> me = people.uid( :mgranger )
# => #<Treequel::Branch:0x19a4970 uid=mgranger,ou=people,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry=nil>
irb> me.dn
# => "uid=mgranger,ou=people,dc=acme,dc=com"
Fetching a branch for uid=mgranger,ou=people.

You can pass any additional attributes in the RDN (if you have entries with multi-value RDNs) as a Hash:

irb> hosts = dir.ou( :hosts )
# => #<Treequel::Branch:0x19a76d4 ou=hosts,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry=nil>
irb> hosts.cn( :ns1, :l => 'newyork' ).dn
# => "cn=ns1+l=newyork,ou=hosts,dc=acme,dc=com"
Fetching a branch for an entry with a multi-value RDN.

You can also create a Branch from the directory that contains it and its full DN:

irb> me = Treequel::Branch.new( dir, 'uid=mgranger,ou=people,dc=acme,dc=com' )
# => #<Treequel::Branch:0x12b676c uid=mgranger,ou=people,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry=nil>
Fetching a branch using its DN.

or from a raw LDAP::Entry object:

irb> conn = LDAP::SSLConn.new( 'localhost', 389, true )
# => #<LDAP::SSLConn:0x58bd94>
irb> entries = conn.search2( 'ou=people,dc=acme,dc=com', LDAP::LDAP_SCOPE_SUBTREE, '(uid=mgranger)' )
# => [{"gidNumber"=>["200"], "cn"=>["Michael Granger"], [...], "dn"=>["uid=mgranger,ou=People,dc=acme,dc=com"]}]
irb> me = Treequel::Branch.new_from_entry( entries.first, dir )
# => #<Treequel::Branch:0x129dd70 uid=mgranger,ou=People,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry={...}>
Fetching a branch using its DN.

The directory also provides a special Branch object for its base DN that’s used to respond to any Branch methods called on it:

irb> dir.base
# => #<Treequel::Branch:0x1368548 dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry=nil>
irb> dir.base.dn
# => "dc=laika,dc=com"
irb> dir.dn
# => "dc=laika,dc=com"
irb> dir.base.ou( :people )
# => #<Treequel::Branch:0x117f45c ou=people,dc=laika,dc=com @ localhost:389 (dc=laika,dc=com, tls, anonymous) entry=nil>
irb> dir.ou( :people )
# => #<Treequel::Branch:0x11850f0 ou=people,dc=laika,dc=com @ localhost:389 (dc=laika,dc=com, tls, anonymous) entry=nil>
Fetching the directory's base branch.

Branches are also returned as the results from a search, but that will be covered a little later.

The Branch’s Entry

Once you have a Branch, you can fetch its corresponding entry from the directory via the #entry method. If the entry doesn’t exist, #entry will return nil. You can test to see whether an entry for a branch exists via its #exists? predicate method:

irb> www = dir.ou( :hosts ).cn( :www )
# => #<Treequel::Branch:0x1932f8c cn=www,ou=hosts,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, cn=admin,dc=acme,dc=com) entry=nil>
irb> www.exists?
# => true
irb> www.entry
# => {"cn"=>["www"], "ipHostNumber"=>["127.0.0.1"], "objectClass"=>["device", "ipHost"], "dn"=>["cn=www,ou=hosts,dc=acme,dc=com"]}
Examining a Branch's entry.

Attributes

Once you have a the Branch for the entry you need, you can also fetch its attributes just like a Hash:

irb> me = people.uid( :mgranger )
irb> me[:gecos]
# => "Michael Granger"
Fetching an attribute.

If you have write privileges on the entry, you can set attributes the same way:

irb> dir.bound_as( me, 'password' ) { me[:gecos] = "Pasoquod Singular" }
irb> me[:gecos] 
# => "Pasoquod Singular"
Setting an attribute.

Changes to the branch are written to the directory as soon as they’re made, so if you have several attributes to change, you’ll likely want to make them all at once for efficiency.

You can do that with the #merge method:

irb> me.merge( :gecos => 'Michael Granger', :uidNumber => 514 )
Merging attributes.

Attribute Datatypes

Attribute values are cast to Ruby objects based on the syntax rule that corresponds to its attribute type. By default, all values are mapped to Strings except those contained in Treequel::Directory::DEFAULT_SYNTAX_MAPPING, which is a mapping of syntax rule OIDs to an object which converts the String value from the directory into a Ruby object. This object can be of any type which responds to #[] with a String argument, e.g., a Proc, a Hash, a Method, etc.:

irb> dir.add_syntax_mapping( Treequel::Constants::OIDS::INTEGER_SYNTAX ) {|string| Integer(string) }
# => #<Proc:0x00507080@(irb):3>
Convert integer values to Ruby Integers

You can use this, for example, to return every attribute that’s a DN as a Treequel::Branch instead of the DN string:

irb> dir = Treequel.directory 
# => #<Treequel::Directory:0x665783 localhost:389 (connected) base_dn="dc=acme,dc=com", bound as=anonymous, schema=(schema not loaded)>
irb> dir.add_syntax_mapping( Treequel::Constants::OIDS::DISTINGUISHED_NAME_SYNTAX ) {|dn| Treequel::Branch.new(dir, dn) }
# => #<Proc:0x0198ddd8@(irb):2>
irb> isdept = dir.ou( :departments ).cn( :sales )
# => #<Treequel::Branch:0x18def54 cn=sales,ou=departments,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry=nil>
irb> isdept['supervisor']
# => #<Treequel::Branch:0x18db228 uid=mahlon,ou=People,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry=nil>
Mapping DNs to Treequel::Branches by default

Syntax mappings are per-directory, so you can establish different conversion rules for different directories. We’re planning on adding a way to add or replace the default rules, too, but for now you’ll have to either modify Treequel::Directory::DEFAULT_SYNTAX_MAPPING directly or install custom mappings for each Directory individually if you want the same rules for every one.

Operational Attributes

In addition to its user-settable attributes, each entry in the directory also has a set of operational attributes which are maintained by the server. These attributes are not normally visible, but you can enable them either for individual Branch objects, or for all newly-created Branches:

irb> dir.base.entry.keys.sort
# => ["dc", "description", "dn", "o", "objectClass"]
irb> dir.base.include_operational_attrs = true
# => true
irb> dir.base.entry.keys.sort
# => ["createTimestamp", "creatorsName", "dc", "description", "dn", "entryCSN", "entryDN", "entryUUID", "hasSubordinates", "modifiersName", "modifyTimestamp", "o", "objectClass", "structuralObjectClass", "subschemaSubentry"]
irb> Treequel::Branch.include_operational_attrs = true
# => true
irb> dir.ou( :people ).entry.keys.sort
# => ["createTimestamp", "creatorsName", "description", "dn", "entryCSN", "entryDN", "entryUUID", "hasSubordinates", "modifiersName", "modifyTimestamp", "objectClass", "ou", "structuralObjectClass", "subschemaSubentry"]
Enabling the inclusion of operational attributes.

Getting a Branch’s LDIF

A convenient way to look at all of a branch’s attributes is via its LDIF string:

irb> puts me.to_ldif
dn: uid=mgranger,ou=people,dc=acme,dc=com
gidNumber: 200
cn: Michael Granger
l: Portland, OR
givenName: Michael
title: Lead Software Developer
gecos: Michael Granger
homeDirectory: /home/m/mgranger
uid: mgranger
mail: mgranger@acme.com
sn: Granger
mobile: +1 9075551212
loginShell: /bin/base
uidNumber: 2053
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
objectClass: posixAccount
objectClass: shadowAccount
objectClass: apple-user
homePhone: +1 9075551212
departmentNumber: 18
Displaying a branch's LDIF.

Parents and Children

Each branch can also fetch its parent and its children:

irb> marketing_hosts = dir.dc( :marketing ).ou( :hosts )
# => #<Treequel::Branch:0x135ad94 ou=hosts,dc=marketing,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry=nil>
irb> marketing_hosts.parent
# => #<Treequel::Branch:0x13508d0 dc=marketing,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry=nil>
irb> marketing_hosts.children.map {|b| b.dn }
# => ["cn=rockbox,ou=Hosts,dc=marketing,dc=acme,dc=com", "cn=bone,ou=Hosts,dc=marketing,dc=acme,dc=com"]
Fetching a branch's parent and its children.

Creating, Copying, Deleting, and Moving Entries

If you have a Branch object for an entry which doesn’t exist in the directory, you can create it via the #create method. It takes any attributes that should be set when creating it, and requires at least that you provide a structural objectClass.

irb> mahlon_things = dir.ou( :people ).uid( :mahlon ).ou( :things )
# => #<Treequel::Branch:0xfb7b9e954 ou=things,uid=mahlon,ou=people,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry=nil>
irb> mahlon_things.create( :objectClass => [ 'top', 'organizationalUnit' ] )
# => true
irb> mahlon_things.cn( :thing ).create( :objectClass => 'room', :description => 'a thing' )
# => true
Creating a new entry.

Some objectClasses require that the entry contain values for particular attributes (their MUST attributes), and it’s nice to be able to tell which ones you’ll need if you’re building tools that can create new entries. To that end, Treequel comes with a set of tools for fetching and using the information contained in a Directory’s schema. We’ll cover schema introspection a little later.

You can also copy an existing entry:

irb> dir.bind( 'cn=admin,dc=acme,dc=com', 'the_password' )
# => "cn=admin,dc=acme,dc=com"
irb> jtran = dir.ou( :people ).uid( :mahlon ).copy( 'uid=jtran', :givenName => 'Jim', :sn => 'Tran' )
# => #<Treequel::Branch:0x12bff60 uid=jtran,ou=people,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, cn=admin,dc=acme,dc=com) entry=nil>
irb> jtran['sn']
# => ["Tran"]
Copying an entry.

or move (rename) it:

# Rename 'Miriam Robson' to 'Miriam Price' when she gets married.
irb> user = dir.ou( :people ).uid( :mrobson ).move( 'uid=mprice', :sn => 'Price' )
# => #<Treequel::Branch:0x12bff60 uid=mprice,ou=people,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, cn=admin,dc=acme,dc=com) entry=nil>
Moving an entry.

Treequel currently doesn’t support moving an entry to a new parent, only renaming it under its current parent, but you can always copy it to the new DN and delete the original. This will be corrected in a future version.

Finally, you can delete an entry from its Branch, as well:

irb> dir.ou( :hosts ).cn( :ns1 ).delete
# => true
Deleting an entry.

Searching With Branchsets

If you know exactly which entries you need, it’s pretty easy to fetch the corresponding Branch objects, but what if you need to search for entries matching one or more criteria?

Searching is implemented in Treequel via Treequel::Branchsets. Much like Datasets from the Sequel library which inspired Treequel, a Branchset is an object which represents an abstract set of records returned by a search. The results of the search are returned on demand, so a Branchset can be kept around and reused indefinitely.

You can construct a new Branchset via the usual constructor; it takes the Branch for the base DN of the search:

irb> Treequel::Branchset.new( dir.ou(:people) )
# => #<Treequel::Branchset:0x1a418ec base_dn='ou=people,dc=acme,dc=com', filter=(objectClass=*), scope=subtree, select=*, limit=0, timeout=0.000>
Creating a new Branchset.

There are also several convenience methods on Branch and Directory that can create a new Branchset relative to themselves, as well:

irb> dir.branchset
# => #<Treequel::Branchset:0x1a3fc54 base_dn='dc=acme,dc=com', filter=(objectClass=*), scope=subtree, select=*, limit=0, timeout=0.000>
irb> dir.ou(:people).branchset
# => #<Treequel::Branchset:0x1998314 base_dn='ou=people,dc=acme,dc=com', filter=(objectClass=*), scope=subtree, select=*, limit=0, timeout=0.000>
Creating new Branchsets relative to the base DN and ou=Hosts.

Like Sequel Datasets, Branchsets are meant to be chainable, so you can refine what entries it will find by calling one of its mutators. Each mutator method returns a new Branchset with the new criteria set. This allows you to build up a query for what you need gradually, in a concise and flexible manner.

Filter

The first of these mutators is #filter.

You can narrow the results of that search by adding one or more filter statements. Each call to #filter adds a clause to the LDAP filter string that is eventually sent to the server.

With no modifications, a Branchset will find every entry below its base using a filter of (objectClass=*) (which will match every entry).

The #filter method expects one or more expressions which are transformed into an LDAP filter, and can be a literal filter String, a Hash or an Array of criteria, or a Ruby expression.

The simplest of these, of course, is a literal LDAP filter in a String:

irb> dir.ou( :people ).filter( '(objectClass=room)' )
=> #<Treequel::Branchset:0x12b7c48 base_dn='ou=people,dc=acme,dc=com', filter=(objectClass=room), scope=subtree, select=*, limit=0, timeout=0.000>
Literal string filter expression

You can see what the equivalent filter of a Branchset is at any time using its #filter_string method:

irb> dir.ou( :people ).filter( '(objectClass=room)' ).filter_string
# => "(objectClass=room)"
Literal string filter expression

You can also use a Hash to do simple attribute=value matching:

irb> dir.ou( :people ).filter( :givenName => 'Michael' ).filter_string
# => "(givenName=Michael)"
Hash filter expression

Multiple criteria in a Hash will be ANDed together:

irb> dir.ou( :people ).filter( :givenName => 'Michael', :sn => 'Granger' )
# => "(&(givenName=Michael)(sn=Granger))"
Multi-value Hash filter expression

You can include an OR in a filter by passing :or as the first element:

irb> dir.ou( :people ).filter( :or, [:sn, 'Granger'], [:sn, 'Smith'] ).filter_string
# => "(|(sn=Granger)(sn=Smith))"
An ORed filter

or by specifying more than one value for a single attribute:

# => #<Treequel::Directory:0x4e45d5 localhost:389 (connected) base_dn="dc=acme,dc=com", bound as=anonymous, schema=(schema not loaded)>
irb> dir.ou( :people ).filter( :uid => [:mahlon, :mgranger, :jtran] ).filter_string
ORing with a Hash

You can do the same with :and and :not, and combine them, too:

irb> dir.ou( :people ).filter( :and, [:sn, 'Granger'], [:sn, 'Smith'] ).filter_string
# => "(&(sn=Granger)(sn=Smith))"
irb> dir.ou( :people ).filter( :not, [:and, [:sn, 'Granger'], [:sn, 'Smith']] ).filter_string
# => "(!(&(sn=Granger)(sn=Smith)))"
Negation (NOT) of an explicit AND

Because filter returns the mutated branchset, you can always chain them together instead of using an explicit :and.

irb> dir.ou( :people ).filter( :objectClass => 'inetOrgPerson' ).filter( :sn => 'Smith' ).filter_string
# => "(&(objectClass=inetOrgPerson)(sn=Smith))"
Chaining filter expressions ANDs them, too.

We’re experimenting with support for Sequel expressions for more-complex filter expressions, too:

# Negative 
irb> dir.ou( :people ).filter( ~:photo ).filter_string
# => "(!(photo=*))"
irb> dir.ou( :people ).filter( :employeeNumber <= 1000 ).filter_string
# => "(employeeNumber<=1000)"
irb> dir.ou( :people ).filter( :sn.like('smith') ).filter_string
# => "(sn~=smith)"
irb> dir.ou( :people ).filter( :sn.like('sm*') ).filter_string
# => "(sn=sm*)"
irb> dir.ou( :people ).filter( :sn => ['smith', 'tran'] ).filter_string
# => "(|(sn=smith)(sn=tran))"
Advanced expressions

Scope

You can also create a Branchset that will search using a different scope by passing :onelevel, :base, or :subtree (the default) to the #scope method of the original Branchset:

Setting the scope to :onelevel (as you might expect) means that it will only descend one level when searching:

irb> dir.filter( :objectClass => :organizationalUnit ).scope( :one ).collect {|branch| branch[:ou].first }
=> ["Hosts", "Groups", "Lists", "Resources", "People", "Departments", "Netgroups"]
Find all the top-level OUs

Setting it to :subtree (which is the default) means that it will descend infinitely, and setting it to :base means that it will only consider the base entry, either returning it if it matches, or returning nil if it does not.

Limit

Setting a Branchset’s #limit will limit the number of results the search will return.

irb> dir.ou( :groups ).limit( 5 ).collect {|b| b.dn }
# => ["ou=Groups,dc=acme,dc=com", "cn=anim,ou=Groups,dc=acme,dc=com", "cn=acct,ou=Groups,dc=acme,dc=com", "cn=mailuser,ou=Groups,dc=acme,dc=com", "cn=producer,ou=Groups,dc=acme,dc=com"]
Return the first 5 groups in the directory

Note that the results will be returned in directory order (at least in OpenLDAP). Until Treequel supports server-side ordering, this means that #limit is of limited usefulness; to do real paged results you need both server-side ordering and the paged results control.

We’re planning on adding a convenient way to use controls in a future release.

If you already have a Branchset with a limit, and want a new one that won’t have any limits imposed on it, you can get one via the #without_limit method.

irb> fivegroups = dir.ou( :groups ).limit( 5 )
# => #<Treequel::Branchset:0x1264908 base_dn='ou=groups,dc=acme,dc=com', filter=(objectClass=*), scope=subtree, select=*, limit=5, timeout=0.000>
irb> fivegroups.all.length
# => 5
irb> fivegroups.without_limit.all.length
# => 99
Making a Branchset without the limits of the original

Select

If you should want to limit the attributes that are returned in the entries fetched by the query, you can do so by specifying which ones should be returned with the #select method:

irb> dir.ou( :people ).select( :sn, :givenName ).limit( 5 ).collect {|b| b.entry }
# => [{"dn"=>["ou=People,dc=acme,dc=com"]}, {"givenName"=>["Reed"], "sn"=>["Slimlocke"], "dn"=>["uid=rslim,ou=People,dc=acme,dc=com"]}, {"givenName"=>["Jim"], "sn"=>["Tran"], "dn"=>["uid=jtran,ou=People,dc=acme,dc=com"]}, {"givenName"=>["Michael"], "sn"=>["Granger"], "dn"=>["uid=mgranger,ou=People,dc=acme,dc=com"]}, {"givenName"=>["Harken"], "sn"=>["Farkselstein"], "dn"=>["uid=hfarkselstein,ou=People,dc=acme,dc=com"]}]
Fetch only employee first and last names

You can get a copy of a Branchset with additional attributes by passing the additional attributes to #select_more:

irb> people_uids = dir.ou( :people ).select( :uid )
# => #<Treequel::Branchset:0x1181644 base_dn='ou=people,dc=acme,dc=com', filter=(objectClass=*), scope=subtree, select=uid, limit=0, timeout=0.000>
irb> people_uids_and_names = people_uids.select_more( :gecos )
# => #<Treequel::Branchset:0x1178b20 base_dn='ou=people,dc=acme,dc=com', filter=(objectClass=*), scope=subtree, select=uid,gecos, limit=0, timeout=0.000>
irb> people_uids_names_and_addresses = people_uids.select_more( :gecos, :homePostalAddress )
# => #<Treequel::Branchset:0x10dcb08 base_dn='ou=people,dc=acme,dc=com', filter=(objectClass=*), scope=subtree, select=uid,gecos,homePostalAddress, limit=0, timeout=0.000>
Selecting additional attributes

You can also get a copy with the select-list removed:

irb> people_uids.select_all
# => #<Treequel::Branchset:0x10da308 base_dn='ou=people,dc=acme,dc=com', filter=(objectClass=*), scope=subtree, select=*, limit=0, timeout=0.000>
Removing the selection from a branchset

Timeout

To avoid unintentional resource consumption on the server, you can specify an explicit timeout for queries. This is useful when searching with user submitted input or other untrusted sources. Note that this can only be reliably used to decrease the timeout, as the server might have a maximum timeout configured that can’t be exceeded.

irb> dir.filter('objectClass=*').timeout( 1 ).all
LDAP::ResultError: Timed out
    from ./treequel/directory.rb:328:in `search_ext2'
    from ./treequel/directory.rb:328:in `search'
    from ./treequel/branchset.rb:195:in `each'
    from (irb):8:in `all'
    from (irb):8
    from :0
A long running query

If you have a canned query that includes a timeout, you can copy it without the restriction.

irb> slow_query = dir.filter('objectClass=*').timeout( 1 )
# => #<Treequel::Branchset:0x1d5c554 base_dn='dc=acme,dc=com', filter=(objectClass=*), scope=subtree, select=*, limit=0, timeout=1.000>
irb> slow_query.all
LDAP::ResultError: Timed out
    from ./treequel/directory.rb:328:in `search_ext2'
    from ./treequel/directory.rb:328:in `search'
    from ./treequel/branchset.rb:195:in `each'
    from (irb):13:in `all'
    from (irb):13
    from :0
irb> slow_query.without_timeout.all.length
# => 4982
irb> slow_query.without_timeout.all.first
# => #<Treequel::Branch:0x1d4f2f0 dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry={"o"=>["ACME"], "description"=>["http://www.example.com/"], "objectClass"=>["dcObject", "organization"], "dc"=>["acme"], "dn"=>["dc=acme,dc=com"]}>
A long running query with timeout disabled

Branchset Enumeration

Branchsets are also Enumerable, so you can slice and dice results with its interface:

irb> people = dir.ou( :people )
# => #<Treequel::Branch:0x11857d0 ou=people,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry=nil>
irb> people.all? {|person| File.directory?(person[:homeDirectory]) }
NoMethodError: undefined method `all?' for #<Treequel::Branch:0x11857d0>
    from /Users/mgranger/source/ruby/Treequel/lib/treequel/branch.rb:538:in `method_missing'
    from (irb):3
irb> people.filter( :homeDirectory ).all? {|person| File.directory?(person[:homeDirectory]) }
# => false
irb> people.filter( :homeDirectory ).find_all {|person| File.exist?(person[:homeDirectory]) && File.stat(person[:homeDirectory]).uid != person[:uidNumber] }
# => [#<Treequel::Branch:0x18287b8 uid=wwwspider,ou=People,dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry={"cn"=>["Auth account for web spider"], "gidNumber"=>["200"], "givenName"=>["WebSpider"], "gecos"=>["WebSpider Account"], "homeDirectory"=>["/dev/null"], "sn"=>["WebSpider Account"], "uid"=>["wwwspider"], "uidNumber"=>["1500"], "objectClass"=>["top", "person", "inetOrgPerson", "posixAccount", "shadowAccount"], "dn"=>["uid=wwwspider,ou=People,dc=acme,dc=com"]}>]
Enumerating resulting Branches

For convenience, the Branchset#map method is overriden to facilitate fetching single attributes from the resulting branches:

irb> dir.ou( :hosts ).filter( :ipHostNumber ).map( :ipHostNumber ).flatten
=> ["192.168.1.253", "192.168.1.14", "192.168.1.21", "192.168.1.22", "192.168.1.23"]
Mapping branch attributes

Branch Collections

So far we’ve been searching from a single base DN, but sometimes what you want is located in different branches of the directory.

For example, hosts might be listed under different domainComponents under the base that correspond to subdomains:

irb> dir.filter( :objectClass => 'dcObject' ).scope( :one ).map( :dc )
# => ["sales", "marketing", "admin", "it", "vpn"]

Treequel::BranchCollections can be used to form searches from multiple bases. They can be constructed from one or more Branchsets:

irb> collection = Treequel::BranchCollection.new( dir.dc(:marketing).branchset, dir.dc(:sales).branchset )
# => #<Treequel::BranchCollection:0x10ef8c0 2 branchsets: ["dc=marketing,dc=acme,dc=com/(objectClass=*)", "dc=sales,dc=acme,dc=com/(objectClass=*)"]>
Creating a collection from explicit Branchsets

or directly from Branches, which will be converted to Branchsets:

irb> collection = Treequel::BranchCollection.new( dir.dc(:marketing), dir.dc(:sales) )
# => #<Treequel::BranchCollection:0x10c2dfc 2 branchsets: ["dc=marketing,dc=acme,dc=com/(objectClass=*)", "dc=sales,dc=acme,dc=com/(objectClass=*)"]>
Creating BranchCollection based on the results of a search

or via Treequel::Branchset’s #collection method:

irb> collection = dir.scope(:one).filter(:objectClass => 'dcObject', :dc => ['sales', 'marketing']).collection
# => #<Treequel::BranchCollection:0x50b644 2 branchsets: ["dc=marketing,dc=acme,dc=com/(objectClass=*)", "dc=sales,dc=acme,dc=com/(objectClass=*)"]>
A more-convenient way to turn the results returned by a Branchset into a collection.

You can also compose BranchCollections by appending new Branchsets:

irb> coll = Treequel::BranchCollection.new
# => #<Treequel::BranchCollection:0x1021420 0 branchsets: []>
irb> coll << dir.dc( :sales )
# => #<Treequel::BranchCollection:0x1021420 1 branchsets: ["dc=sales,dc=acme,dc=com/(objectClass=*)"]>
irb> coll << dir.dc( :marketing )
# => #<Treequel::BranchCollection:0x1021420 2 branchsets: ["dc=sales,dc=acme,dc=com/(objectClass=*)", "dc=marketing,dc=acme,dc=com/(objectClass=*)"]>
Building up a BranchCollection gradually

or by adding one BranchCollection to another:

irb> east_coast = dir.filter( :dc => [:admin, :it] ).collection
# => #<Treequel::BranchCollection:0x594aac 2 branchsets: ["dc=it,dc=acme,dc=com/(objectClass=*)", "dc=admin,dc=acme,dc=com/(objectClass=*)"]>
irb> west_coast = dir.filter( :dc => [:sales, :marketing] ).collection
# => #<Treequel::BranchCollection:0x55d980 2 branchsets: ["dc=marketing,dc=acme,dc=com/(objectClass=*)", "dc=sales,dc=acme,dc=com/(objectClass=*)"]>
irb> national = east_coast + west_coast
# => #<Treequel::BranchCollection:0x554ec0 4 branchsets: ["dc=it,dc=acme,dc=com/(objectClass=*)", "dc=admin,dc=acme,dc=com/(objectClass=*)", "dc=marketing,dc=acme,dc=com/(objectClass=*)", "dc=sales,dc=acme,dc=com/(objectClass=*)"]>
Combining two BranchCollections into one

BranchCollections work via delegation to their Branchsets, so all of the mutator methods on Branchset are supported by BranchCollection. This means that you can chain collections and filters together, with collections serving as the base for further finer-grained searches:

dir.filter( :ou => 'hosts' ).collection.filter( :cn => 'www' )
Find all hosts named 'www' under all @ou=hosts@ branches

Schema Introspection

The information about the structure of the directory comes from its schema, and Treequel provides instrospection tools for accessing it in an object-oriented manner. You can get the Treequel::Schema from the directory by calling its #schema method:

irb> dir.schema
# => #<Treequel::Schema:0x66511b 1119 attribute types, 31 ldap syntaxes, 54 matching rule uses, 72 matching rules, 310 object classes>
Fetching the schema for a Directory.

Object Classes

You can fetch information about the objectClasses the directory knows about through the schema’s #object_classes Hash:

irb> dir.schema.object_classes[:inetOrgPerson] 
# => #<Treequel::Schema::StructuralObjectClass:0x65d91b inetOrgPerson(2.16.840.1.113730.3.2.2) < "organizationalPerson" "RFC2798: Internet Organizational Person" MUST: [], MAY: [:audio, :businessCategory, :carLicense, :departmentNumber, :displayName, :employeeNumber, :employeeType, :givenName, :homePhone, :homePostalAddress, :initials, :jpegPhoto, :labeledURI, :mail, :manager, :mobile, :o, :pager, :photo, :roomNumber, :secretary, :uid, :userCertificate, :x500uniqueIdentifier, :preferredLanguage, :userSMIMECertificate, :userPKCS12]>
Fetching an ObjectClass object for the 'inetOrgPerson' objectClass.

This hash is keyed by both OID and any associated names (as Symbols), and the value is a Treequel::Schema::ObjectClass object that contains the information about that objectClass parsed from the schema.

irb> inetOrgPerson = dir.schema.object_classes[:inetOrgPerson] 
# => #<Treequel::Schema::StructuralObjectClass ...>
irb> inetOrgPerson.oid
# => "2.16.840.1.113730.3.2.2"
irb> inetOrgPerson.names
# => [:inetOrgPerson]
irb> inetOrgPerson.may_oids
# => [:audio, :businessCategory, :carLicense, :departmentNumber, :displayName, :employeeNumber, :employeeType, :givenName, :homePhone, :homePostalAddress, :initials, :jpegPhoto, :labeledURI, :mail, :manager, :mobile, :o, :pager, :photo, :roomNumber, :secretary, :uid, :userCertificate, :x500uniqueIdentifier, :preferredLanguage, :userSMIMECertificate, :userPKCS12]
irb> inetOrgPerson.desc
# => "RFC2798: Internet Organizational Person"
irb> inetOrgPerson.sup
# => #<Treequel::Schema::StructuralObjectClass:0x65fe6e person(2.5.6.6) < #<Treequel::Schema::AbstractObjectClass:0x6637ad top(2.5.6.0) < nil "top of the superclass chain" MUST: [:objectClass], MAY: []> "RFC2256: a person" MUST: [:sn, :cn], MAY: [:userPassword, :telephoneNumber, :seeAlso, :description]>
Introspection on the inetOrgPerson objectClass.

Treequel::Branch objects provide a shortcut for looking up the Treequel::ObjectClass objects that correspond to its objectClass properties:

irb> dir.base.object_classes
# => [#<Treequel::Schema::AuxiliaryObjectClass:0x68b168 dcObject(1.3.6.1.4.1.1466.344) < #<Treequel::Schema::AbstractObjectClass:0x690555 top(2.5.6.0) < nil "top of the superclass chain" MUST: [:objectClass], MAY: []> "RFC2247: domain component object" MUST: [:dc], MAY: []>, #<Treequel::Schema::StructuralObjectClass:0x68d02b organization(2.5.6.4) < #<Treequel::Schema::AbstractObjectClass:0x690555 top(2.5.6.0) < nil "top of the superclass chain" MUST: [:objectClass], MAY: []> "RFC2256: an organization" MUST: [:o], MAY: [:userPassword, :searchGuide, :seeAlso, :businessCategory, :x121Address, :registeredAddress, :destinationIndicator, :preferredDeliveryMethod, :telexNumber, :teletexTerminalIdentifier, :telephoneNumber, :internationaliSDNNumber, :facsimileTelephoneNumber, :street, :postOfficeBox, :postalCode, :postalAddress, :physicalDeliveryOfficeName, :st, :l, :description]>]
Fetching the objectClasses for an entry through its Branch.

Attribute Types

You can also fetch introspection information on entry attributeTypes via the schema’s #attribute_types Hash:

irb> dir.schema.attribute_types[:surname]
# => #<Treequel::Schema::AttributeType:0x146abd sn(2.5.4.4) "RFC2256: last (family) name(s) for which the entity is known by" SYNTAX: nil (length: unlimited)>
Fetching an AttributeType object for the 'surname' attribute.

Like with objectClasses, they are keyed both by numeric OID strings and their associated names (as Symbols), and the values are instances of Treequel::Schema::AttributeType.

irb> sn = dir.schema.attribute_types[:surname]
# => #<Treequel::Schema::AttributeType:0x696ec8 sn(2.5.4.4) "RFC2256: last (family) name(s) for which the entity is known by" SYNTAX: nil (length: unlimited)>
irb> sn.oid
# => "2.5.4.4"
irb> sn.names
# => [:sn, :surname]
irb> sn.desc
# => "RFC2256: last (family) name(s) for which the entity is known by"
irb> sn.obsolete?
# => false
irb> sn.sup
sn.sup_oid   sn.sup_oid=  sn.sup       
irb> sn.sup
# => #<Treequel::Schema::AttributeType:0x69e542 name(2.5.4.41) "RFC4519: common supertype of name attributes" SYNTAX: "1.3.6.1.4.1.1466.115.121.1.15" (length: 32768)>
irb> sn.eq
sn.eql?                    sn.eqmatch_oid=            sn.equal?                  sn.equality_matching_rule  
sn.eqmatch_oid             
irb> sn.equal
sn.equal?                  sn.equality_matching_rule  
irb> sn.equality_matching_rule
# => #<Treequel::Schema::MatchingRule:0x687f7c caseIgnoreMatch(2.5.13.2)  SYNTAX: #<Treequel::Schema::LDAPSyntax:0x689043 1.3.6.1.4.1.1466.115.121.1.15(Directory String)>>
irb> sn.substr_matching_rule
# => #<Treequel::Schema::MatchingRule:0x688026 caseIgnoreSubstringsMatch(2.5.13.4)  SYNTAX: nil>
irb> sn.user_modifiable?
# => true
Fetching an AttributeType object for the 'inetOrgPerson' objectClass.

Branches also know how to fetch the attribute types that are allowed by their objectClasses’ MUST and MAY OIDs:

irb> base = dir.base
# => #<Treequel::Branch:0x1a7f8cc dc=acme,dc=com @ localhost:389 (dc=acme,dc=com, tls, anonymous) entry=nil>
irb> base.may_oids
# => [:userPassword, :searchGuide, :seeAlso, :businessCategory, :x121Address, :registeredAddress, :destinationIndicator, :preferredDeliveryMethod, :telexNumber, :teletexTerminalIdentifier, :telephoneNumber, :internationaliSDNNumber, :facsimileTelephoneNumber, :street, :postOfficeBox, :postalCode, :postalAddress, :physicalDeliveryOfficeName, :st, :l, :description]
irb> base.may_attribute_types
# => [#<Treequel::Schema::AttributeType:0x69e1af userPassword(2.5.4.35) "RFC4519/2307: password of user" SYNTAX: "1.3.6.1.4.1.1466.115.121.1.40" (length: 128)>, #<Treequel::Schema::AttributeType:0x6968ce searchGuide(2.5.4.14) "RFC2256: search guide, deprecated by enhancedSearchGuide" SYNTAX: "1.3.6.1.4.1.1466.115.121.1.25" (length: unlimited)>, #<Treequel::Schema::AttributeType:0x69dfa7 seeAlso(2.5.4.34) "RFC4519: DN of related object" SYNTAX: nil (length: unlimited)>, ...]
Fetching an AttributeType object for the 'inetOrgPerson' objectClass.

Other Schema Information

The Schema object also facilitates access to the directory’s syntaxes and matching rules via the Treequel::Schema::LDAPSyntax, Treequel::Schema::MatchingRule, and Treequel::Schema::MatchingRuleUse classes. They are accessed via the #ldap_syntaxes, #matching_rules, and #matching_rule_uses attributes of the Schema, respectively. They, like #object_classes and #attribute_types, are Hashes keyed both by OID and names as Symbols.

Real World Examples

Cross-directory Searches

For example, this one-liner finds the first name of all inetOrgPerson classes within the People organizational unit that have a uid that starts with the string “ma”:

irb> dir.ou( :people ).filter( :objectClass => 'inetOrgPerson' ).filter( :uid => 'ma*' ).collect {|branch| branch[:givenName].first }.sort
# => ["Mahlon", "Margaret", "Margaret", "Mark", "Marlon", "Matt", "Mike", "Mimi"]
Chaining filter methods

Authors

Contributors

A special thanks to Ben Bleything, who was part of the initial brainstorm that led to the creation of this library.

License

Copyright © 2008-2009, Michael Granger and Mahlon E. Smith All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Creative Commons License
The content of this manual, including images, video, and any example source code is licensed under a Creative Commons Attribution 3.0 License.