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)>
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)>
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)>
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)>
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"
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)>
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)>
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"
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"
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"
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>
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={...}>
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>
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"]}
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"
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"
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 )
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>
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>
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"]
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
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"]
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
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"]
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>
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
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>
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>
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>
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)"
You can also use a Hash to do simple attribute=value
matching:
irb> dir.ou( :people ).filter( :givenName => 'Michael' ).filter_string # => "(givenName=Michael)"
Multiple criteria in a Hash will be ANDed together:
irb> dir.ou( :people ).filter( :givenName => 'Michael', :sn => 'Granger' ) # => "(&(givenName=Michael)(sn=Granger))"
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))"
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
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)))"
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))"
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))"
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"]
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"]
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
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"]}]
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>
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>
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
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"]}>
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"]}>]
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"]
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=*)"]>
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=*)"]>
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=*)"]>
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=*)"]>
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=*)"]>
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' )
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>
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]>
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]>
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]>]
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)>
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
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)>, ...]
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"]
Authors
- Michael Granger
- Mahlon E. Smith
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:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of the authors nor contributors may be used to endorse or promote products derived from this software without specific prior written permission.
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.

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