wiki:DeveloperNotes

Treequel Developer Notes

The main idea behind the library came from the syntax spike we did to test the concepts.

There are still some features which we have planned, but aren't yet well-understood enough to implement.

Mapping LDAP Into Ruby's ObjectSpace

An ORM-style mapping for LDAP is fundamentally inappropriate, as an LDAP directory doesn't behave anything like a relational database. LDAP is actually more similar to Ruby's class system than the set-algebra of an RDBMS, so trying to wedge an ORM in between it and Ruby introduces an impedance mismatch that reduces the usefulness of any moderately-complex directory by quite a lot.

Instead, we should embrace the hierarchical nature and the separation of concerns inherent in LDAP's objectClasses. A directory's schema should be used to inform a dynamically-constructed class hierarchy, using Class objects for STRUCTURAL objectClasses and mixed-in Modules for every other kind.

There are several technical hurdles to overcome for a system like this:

  1. A directory's syntax is state that is fetched at runtime, after configuration, and most Ruby class libraries are declared before runtime is entered. This is mitigated quite a bit by the fact that Ruby's classes are also open, and can be extended at will at runtime.
  2. Since sibling entries can have different sets of objectClasses, you can really only decorate an entry as it's fetched from the directory. You can't, for instance, associate the class for inetOrgPerson with every entry at ou=people,dc=acme,dc=com, as there's absolutely no guarantee that an entry fetched from there will include that objectClass.

Given those considerations, I'm thinking it should look something like this:

require 'treequel/model'

class Acme::LdapObject < Treequel::ModelObject

    def self::configure( config )
        self.directory = Treequel.directory( config.ldap.uri )
    end

end

class Acme::Person < Acme::LdapObject
    ldap_class :inetOrgPerson
end

class Acme::Department < Acme::LdapObject
    ldap_class :acmeDepartment
end


# Configure the base class and establish the connection to the directory
config = Acme::Config.load( 'acme.yml' )
Acme::LdapObject.configure( config )

# Return a Treequel::Branchset for (base:ou=people,
# filter:objectClass=inetOrgPerson) that will return Branches wrapped
# inside an instance of the appropriate Treequel::ModelObject.
people = Acme::Person.base( :ou => :people ).all

# or one that only includes people that have an email address
activated_people = Acme::Person.filter( :mail )

# or fetch a single instance by its DN attribute
person = Acme::Person.uid( :msmith )

# Return a Treequel::BranchSet for (base:ou=people with no filter).
# This will return Acme::Person objects for entries that have the
# `inetOrgPerson` objectClass, but also non-Person objects too.
anypeopleobj = Acme::LdapObject.ou( :people )
person = anypeopleobj.uid( :msmith )
nonperson = anypeopleobj.cn( :somethingelse )

# In the case where your departments are broken down into a hierarchy
# under ou=departments, you can fetch a department explicitly through
# attribute-traversal methods.
it_dept = Acme::Department.cn( :corporate ).cn( :technology ).cn( :it )

# Now add the 'person' we looked up earlier as a member of the IT
# department
it_dept.uniqueMember << person