During summer it’s pretty quiet here at Opinsys, because all the schools are on summer break. This allows us to explore with new technologies. So I decided to take a crack at ldapjs as possible OpenLDAP replacement and see of what it is made of. I’m also fairly new to LDAP in general and this seems to be a nice way to learn how LDAP servers work in general.
OpenLDAP
Before going to into ldapjs I will review our current architecture and will point out the pain points with it. We store almost all of our data in OpenLDAP, but most importantly, user data, on which I will focus on now.
We have two availability requirements for the user data. It must be editable through some interface from anywhere and always readable from the school network. Editability is required, because there are various web services that use this data for login and users must be able to change their emails and passwords for those from home or any other place.
Availability in school network is crucial for desktop computer logins. Some schools have very spotty Internet connectivity and it is unacceptable to have broken logins when the Internet connection is down.
We have all the user data stored by a organisation in its own subtree in our OpenLDAP master server which is located in a proper data center in central Finland. From there each organisation subtree is replicated to corresponding OpenLDAP slave server which is in the school’s network.
Developer happiness
Technically this works just fine, but for for us developers working with OpenLDAP to create new applications is far from ideal.
When you setup a Linux machine to authenticate against an OpenLDAP server you have to give access rights for every user directly to it. Since we have other application data in LDAP as well we have to be very careful when setting up ACL rules so that users cannot read or write any data that does not belong to them.
This leads to something I call “LDAP driven development” where every data access is validated by a OpenLDAP ACL rule. Whether it is desktop login or a OAuth login to some web service. Setting up these rules is no fun. They are not exactly designed for large scale application development.
Enter ldapjs
For introduction I suggest that you read the ldapjs post from the Node.js blog. To summarize ldapjs is a framework for building LDAP servers as Express or Sinatra are frameworks for building HTTP servers. It is also similarly very light weight. It does not come with a database or a replication story. It just gives you the tools for interacting with LDAP clients.
I had two weeks to play with this and I wanted to see if I can meet our requirements with it. For database I went with CouchDB for its praised replication features which seems to be a good fit for our uses.
I wanted to design the architecture with minimal interactions with LDAP. Everything should be done with CouchDB and some custom REST APIs. LDAP side of things should be just a CouchDB adapter for those services that can only interact with LDAP servers such as Linux client logins.
Implementation
My target client system was a Ubuntu Precise Pangolin desktop with
libpam-ldapd
, libnss-ldapd
and nslcd
packages to provide LDAP
integration.
Programming with ldapjs is done by writing handlers for LDAP methods, such as
bind
, search
and modify
and by the base the operation should be executed
against. For details checkout the guide on
ldapjs.org.
To provide LDAP server for the Ubuntu client to authenticate against I had to
only care about bind
and search
methods. On a search handler you get a
request object with filter
attribute which contains a parsed LDAP search
filter with a matching function which can be used to test if some Javascript
object matches the filter.
With that it is easy to go through list of objects and send matching ones to the client.
1 2 3 4 5 6 7 8 |
|
But that does not actually scale if you have thousands of users in the database. It’s not reasonable to fetch them all one by one and test for a match. Ideally I should should transform the filter to a query understood by my database, but with CouchDB this is not reasonable, because the search cababilities are not as expressive as the LDAP search filters can be.
Because this time I was targeting only a one LDAP client I was able to take a shortcut and just detect the exact queries required for nslcd to work. I fired up a dummy ldapjs server which printed out every search filter that was made on it. There weren’t that many:
- Get user by UID
- Get user by UID Number
- Get all users the current bind has access to
- Get groups for UID
Then I created few functions with js-schema to detect those cases. This of course results in a broken LDAP server implementation if used with any other client, but as CouchDB LDAP adapter for these Ubuntu clients this is just fine.
Missing bits
Initially I wanted to design the server without any root accounts. On login only the credentials of the user logging in would be used for LDAP bind, but it appears that it not possible, because the user listing must be readable by nslcd before user even enter his password.
There is a way to workaround this. Our current production setup with OpenLDAP uses the credentials to retrieve a Kerberos ticket from our Kerberos servers before making any connections to the OpenLDAP server and only then uses the Kerberos ticket to bind with OpenLDAP. Sadly this is where ldapjs falls short. It does not support the SASL bind mechanism which is required to authenticate with Kerberos tickets. I opened a Github issue about it.
I’d love hear if there a way to configure logins without Kerberos to not require user data until the password is supplied.
Conclusion
Working with ldapjs has been a quite nice experience. It does keep the promise of bringing Web framework style development to LDAP. Although the clients are not that nice as web browsers are. For example nslcd does does over 200 queries to the LDAP server if used without a caching daemon when logging in a single user. Better keep caches warm when handling large schools.
The code I’ve written while experimenting with ldapjs can be found from our Github account:
https://github.com/opinsys/couchldap
Feel free to poke it if CoffeeScript does not upset you too much. For more complete implementation checkout ldapjs-riak from the creators of ldapjs.