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:
- 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.
- 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
