LDAP Data Source now with full CRUD !

So I was making a site up and had to use LDAP as the backend. CakePHP is my favourite PHP framework and a quick Google search returned a link to a CakePHP bakery article which gave me a great starting point. The class basically allows you to read from LDAP, but that’s it. It was good enough to play with, but I quickly got to a point where I really needed to write to LDAP.

I started investigating how I could get a list of attribute types and object classes from LDAP (without parsing the schema files or something hacky like that). I thought I could look into the phpldapadmin project to see how they did it. I’m glad I did! It was just what I was looking for. They saved me a bit of code!

Credits go out to the original author of the class, euphrate aka “euphrate_ylb”, and to the phpldapadmin guys for some of the code in the class.

Anyway, enough rambling. Onto the good stuff:

  • Download the LDAP data source class and save it as /path/to/cakephp/app/models/datasources/ldap_source.php.
  • You’ll need a model. Mine is in /path/to/cakephp/app/models/ldap_user.php. The most important parts are:
var $useDbConfig = 'ldap';
var $primaryKey = 'uid';

Primary Key is an important one (specially when updating & deleting records). In my LDAP tree, I know for a fact that no 2 users will have the same uid, so I used this field. Feel free to choose another unique field (if, for example, you have an Employee ID field and it is unique).

var $defaultObjectClass = 'inetOrgPerson';

The above variable is used to further strict searching when updating/deleting records to an object class too. If you don’t set the above variable, when you update/delete a user it will search for the $primaryKey field in the entire tree. By restricting to an object class of my liking I ensure I’m not going to get any unwanted matches.

Special note when CREATING records

Since there isn’t any easy way to automagically determine what Distinguished Name (DN) a new record will take, I made it so that one of the fields that you have to submit as part of $this->data is ‘_DN_’. For example, in my beforeSave() function of my ldap_user.php model, I have the following:

function beforeSave() {
        $this->data[$this->name]['_DN_'] = 'cn=' . $user[$this->name]['uid'] . ',ou=Some Department,dc=example,dc=com';
}

When deleting or updating a user, remember to set the $this->Model->id field to be the value that you want to match on (based on $primaryKey). In my case, if I set $this->Model->id = ‘foo’, it will search LDAP for (&(objectclass=inetOrgPerson)(uid=foo)) and based on that match, it will update or delete (depending on what action I’m taking).

It’s now 4AM so there’s a good chance most of what I wrote doesn’t make sense. If you don’t know your way around LDAP, this probably didn’t make a whole lot of sense. Feel free to leave a comment and I’ll try and help out (when I get the chance to!)

I must say it feels good to finally contribute back to a project that has saved me sooo many hours of work. CakePHP is truly an EXCELLENT project that has saved me so many hours of work and brought back the pleasure of coding up a site :) I have been reading lots and lots of CakePHP code lately and the more I read it, the more I like it. It has been well thought out.

That’s it for now. If you have any feedback, suggestions, better ways of doing stuff .. please feel free to leave comments!

– Gonzalo (znoG on irc.freenode.net:#cakephp)

Tags: , ,

34 Responses to “LDAP Data Source now with full CRUD !”

  1. Don Voita Says:

    Hi Gonzalo,
    I’m glad you took the time out to develop this. I’m starting to piece this all together and am wondering if you would mind sending me a copy of your CRUD controller and model? It would give me a better idea of how to proceed.

    Thanks,
    Don Voita
    Fellow Baker

  2. Don Voita Says:

    Hi Gonzalo,
    So far, so good. Where do you prefer we post bugs or modifications to your script? Do you have any plans for revisions?

    Thanks,
    Don

  3. Jules Says:

    I can’t get this to work in our environment. We use Active Directory as our LDAP server. The @schema functions don’t work but a couple code modifications get around that. The bigger problem is that I can’t get the C*UD to work.

    We are using Cake 1.2 that talks to Server 2003 as the ldap server source. When I try ->save it returns a bunch of fatal errors including the missing calculate and query datasource funtions, and also the datasource was missing the $startQuote and $endQuote variables (easily added, of course).

    I’m just wondering if anyone else has got the C*UD components working?

    Otherwise outstanding, of course! :)

  4. gservat Says:

    Hi Don,

    Thanks for writing and I’m terribly sorry for taking so long to get back to you. I haven’t made any changes to the class mainly because “it works for me”, however, I’d love to get the class improved by myself and others so if you have any changes to make to it, let’s discuss! I’ll be posting the class at the bakery at some point. I’ve just been real busy lately (hence the delay in replying to your posts) but I will do it.

    How did you go with the class?

  5. gservat Says:

    @Jules: Thanks for writing. I basically wrote the class with OpenLDAP in mind. The __getLDAPschema() function is very much OpenLDAP orientated. There may well be a different way to fetch the schema from an AD server. Unfortunately, I don’t have access to an AD server to play with (infact, I’ve never used Active Directory). I’ll try and somehow get access to an AD server but as I have a few things on my plate, hopefully somebody else who has access to an AD server can get the class working with it and help you out!

  6. Jules Says:

    @gservat: Which version of Cake have you had this running with?

  7. gservat Says:

    @Jules: I’m currently using 1.2.0.6311.

  8. Don Voita Says:

    @gservat

    No problem. I’m getting along well using the class. I’m able to CRUD records on an openldap server, using the cake 1.2.0.7296-rc2.

    I did make a few changes, for example, in the delete method you specify ‘LdapUser’ as your model, which I substituted with ‘$model->name’ to work for my environment.

    I’d like to see how you use the class’s logging methods, etc. Maybe in a future post, or in the bakery?

    Best,
    Don

  9. euphrate_ylb Says:

    First of all, I would like to thank you for your contribution. I have just discovered accidentally your version of LDAPSOURCE. I am very pleased that you reused what I did. I hope you didn’t have too many troubles with my code.

    Since we need a fully functional CRUD datasource, we will definitely try your code. We will send you some feedbacks as soon as possible.

    Finally, maybe we could compile my recent enhancements (cache management) and your CUD operations to release a smart version of LDAP SOURCE on the project official website http://code.google.com/p/ldapsource/

  10. gservat Says:

    @euphrate_ylb: Great to hear from you! I never got around to publishing this class as a bakery article. Your code was fine; it gave me a great starting point and I was sufficiently motivated to write the other parts.

    Your code.google.com idea sounds good to me. We should probably talk on a real-time channel so that I can get setup on it. I’ll drop you a line.

  11. Dennis Krul Says:

    Hey great work! Thanks :)

    I’m trying to use this datasource to update LDAP objects, but I get an error about a undefined calculate method in LdapSource.

    The calculate method does seem to exist in dbo_source.php.

    I’m using cake 1.2.0.7692-rc3 and an openldap server.

    Any pointers?

  12. Dennis Krul Says:

    Disregard my last comment. I got it to work!

    I simply copied the calculate() method from the dbo_source.php class.

    After that I had some problems with updating objects. It seems that this is caused by the beforeSave() method. If I remove that method from the model updating works just fine.

    I haven’t tried creating new objects yet. Will figure that out later :)

  13. mike Says:

    Hi Gonzalo,

    I tried t use your LDAP-class. Nice peace of work!
    I came to a small problem. Maybe you can help.

    Reading data from the ldap just works fine ( index() ). But when I try to save data back to the ldap, there us just nothing happening ( add() ).

    It seems that the create() function inside the LdapSource class is never called. Can you think of a reason?

    I’m using the latest cakephp version (RC3) and a open LDAP directory.

    Thanks in advance.
    Mike

    Here is what I do

    class LdapUsersController extends AppController {
    var $name = ‘LdapUsers’;
    var $helpers = array(‘Html’, ‘Form’);

    function index() {
    $recursive = 1;
    $this->LdapUser->useTable = ‘ou=SomeGroup’;
    $conditions = ‘uid=*’;
    $data = $this->LdapUser->find(‘all’, $conditions);
    print_r($data);
    }

    function add() { ‘
    $this->data[‘LdapUser’][‘cn’] = ‘Hans Mustermann’;
    $this->data[‘LdapUser’][‘sn’] = ‘Mustermann’;
    $this->data[‘LdapUser’][‘uid’] = ‘hamu’;
    $this->data[‘LdapUser’][‘mail’] = ‘MustermannH@firma.de’;
    $this->data[‘LdapUser’][‘objectclass’] = ‘inetOrgPerson’;
    $this->data[‘LdapUser’][‘ou’] = ‘SomeGroup’;
    $this->data[‘LdapUser’][‘_DN_’] = ‘uid=hamu,ou=SomeGroup,dc=xy,dc=com’;
    $this->LdapUser->save($this->data);
    }
    }

    class LdapUser extends AppModel {
    var $name = ‘LdapUser’;
    var $useDbConfig = ‘ldap’;
    var $primaryKey = ‘uid’;
    var $useTable = false;
    //var $defaultObjectClass = ‘inetOrgPerson’;
    var $additionalFilter = ‘objectclass=inetOrgPerson’;
    }

  14. gservat Says:

    Hi Mike! Thanks for writing.

    I tried your code locally and, as expected, it didn’t work. The create() function in the ldap_source.php *is* actually called, but it fails at the end when it tries to add the entry to LDAP. I then added a “print ldap_error($this->connection));” where ldap_add() failed which showed an “Object Class Violation”. This is because no objectclasses made it into the final array sent to ldap_add() (you can add a print_r($fieldsData) just before the ldap_add() call to see what I mean). What happens is that you fill an array with key/value pairs that you want to add to the LDAP record, and the save() function in the CakePHP cake/libs/model/model.php checks each field against the LDAP schema to see if the field exists. If it doesn’t, it discards the field. So, going back to what you pasted:

    $this->data[‘LdapUser’][‘objectclass’] = ‘inetOrgPerson’;

    In the LDAP schema, ‘objectclass’ doesn’t exist, but ‘objectClass’ does. If you upper-case the ‘c’, it should work.

    Hope this helps!
    Gonzalo

  15. gservat Says:

    @Dennis: Sorry for not getting back to you earlier. Glad you figured it out!
    It looks like you’re using a newer version of CakePHP than mine. I’m using 1.2.0.6311 and the calculate() method in dbo_source.php and any calls to it are not part of this version so it makes sense that you got that error.

    Would be good to know if the rest works with the newest CakePHP versions!

    Thanks for writing.

  16. colby Says:

    Whenever i do a find, the fields are being pulled in as all lowercase. i.e. givenname but the field in ldap is givenName and for this to work before a save i have to set it.

    In my edit form i have to do $givenname=$form->text(‘LdapUser.givenname’); for it to pull in the value to the form.

    on controller before save
    $this->data[‘LdapUser’][‘givenName’]=$this->data[‘LdapUser’][‘givenname’];

    if i do this it will work. any suggestions on how to correct the problem?
    im using a sun java ldap server and i got the code to work with it minus the field case being made all lowercase. Any ideas?

  17. colby Says:

    also getting an error on add
    Notice (8): Undefined property: LdapSource::$startQuote [CORE/cake/libs/model/model.php, line 906]
    Notice (8): Undefined property: LdapSource::$endQuote [CORE/cake/libs/model/model.php, line 906]

    the error is on
    $column = str_replace(array($db->startQuote, $db->endQuote), ”, $column);
    which is in the getColumnType function. Doing some debuging and the create function in ldap source is not being called but this could be due to the error? any suggestions for this as well.

  18. colby Says:

    figured my 2nd question out, debugged a lot and found out my schema for ldap add was just off some, our server requires certain things.

    the first one though still stands with the fields coming in all lowercase instead of camel cased.

  19. Signets remarquables du 06/11/2008 au 13/11/2008 | Cherry on the... Says:

    […] LDAP Data Source now with full CRUD ! « TBlog […]

  20. egelados Says:

    i m trying to get this to work, but i get this error.

    Fatal Error (256): Unable to load DataSource file dbo\dbo_ldap.php [CORE\cake\libs\model\connection_manager.php, line 178]
    Code | Context
    $connName = “ldap”
    $_this = ConnectionManager
    ConnectionManager::$config = DATABASE_CONFIG object
    ConnectionManager::$_dataSources = array
    ConnectionManager::$_connectionsEnum = array
    ConnectionManager::$_log = NULL
    $connections = array(
    “default” => array(
    “filename” => “dbo\dbo_mysql”,
    “classname” => “DboMysql”,
    “parent” => array()
    ),
    “ldap” => array(
    “filename” => “dbo\dbo_ldap”,
    “classname” => “DboLdap”,
    “parent” => array()
    ),
    “test” => array(
    “filename” => “dbo\dbo_mysql”,
    “classname” => “DboMysql”,
    “parent” => array()
    )
    )
    $conn = array(
    “filename” => “dbo\dbo_ldap”,
    “classname” => “DboLdap”,
    “parent” => array(
    “filename” => “dbo_source”,
    “classname” => “DboSource”,
    “parent” => null
    )
    )
    $error = “Unable to load DataSource file %s.php”
    } else {
    $error = __(‘Unable to load DataSource file %s.php’, true);
    trigger_error(sprintf($error, $conn[‘filename’]), E_USER_ERROR);
    ConnectionManager::loadDataSource() – CORE\cake\libs\model\connection_manager.php, line 178
    ConnectionManager::getDataSource() – CORE\cake\libs\model\connection_manager.php, line 105
    Model::find() – CORE\cake\libs\model\model.php, line 1909
    LdapUsersController::index() – APP\controllers\ldapusers_controller.php, line 10
    Object::dispatchMethod() – CORE\cake\libs\object.php, line 115
    Dispatcher::_invoke() – CORE\cake\dispatcher.php, line 245
    Dispatcher::dispatch() – CORE\cake\dispatcher.php, line 211
    [main] – APP\webroot\index.php, line 88

    . . . .. . .

    why does ldap source not get called ??
    why is dbo_xxx source get called instead ??

    i hope you can help

  21. egelados Says:

    i got over the previous problem but now i ve got new ones :)

    add and view dont seem to work. in fact they seem to do absolutely nothing.

    i ll try to debug ldap, see what really happens

  22. egelados Says:

    thanx for getting back Gonzalo.

    it would be really helpful if you were kind enough to provide us with an example controller that calls read,create,update and delete.

    specifically i m having trouble calling the read method correctly, but a general example would be appreciated by most newbies in cake and oop like me.

    :)

  23. egelados Says:

    can someone please provide a working example of view and add function for controller?

    my attempts to call the read method of the ldapsource fail . :S

  24. Georlitz Says:

    Good work on the collaboration in creating this code. It seems like it has some good potential but I’m having problems similar to maybe what egelados is seeing. Specifically I can’t get the read() function to work correctly. Is there a specific format the conditions array should be in for a Model->find(‘all’, array(‘conditions’ => array())) call? If I do something like $conditions = array(‘cn’ => ‘someCN’); I get a “Can’t connect to LDAP Server” error. I think it’s a generic error though since if I run Model->find(‘all’) without conditions, it works just find but returns every record.

    I’ve traced the problem to function __conditionsArrayToString($conditions) {}. For some reason, no matter what format I put my $conditions array in, it never passes the following line:
    if (!in_array($operand,array_keys($ops)) )
    return null;
    ie – it always returns null. If I comment out the conditions parser and force a string: ‘(cn=someCN)’, everything works as one would expect.

    Does anyone have any enlightenment here? Am I passing in the conditions array incorrectly? Or is there perhaps something else at work here? FYI, I’m running on PHP 5.2.6, and Cake 1.2.1.8004.

  25. Philippe Says:

    Hi Gonzalo,

    Thanks for this very useful ldap Class.
    Since I am using it for authenticating over ldap (resp. AD), I do not want to transmit passwords in clear text.
    It is actually very easy to establish an encrypted connection with the ldap_start_tls function.
    You may therefore add a few lines to your connect() function to use TLS.
    I defined an additional parameter ‘tls’ in the array describing ldap in database.php:
    var $ldap = array (
    ‘datasource’ => ‘ldap’,
    ….
    ‘tls’ => true
    );
    and added the following lines into the connect() function of ldap_source.php:
    function connect() {

    ldap_set_option …..
    // *** new ***
    if ($config[‘tls’]) {
    if (!ldap_start_tls($this->connection)) {
    fatal_error(“Ldap_start_tls failed”);
    }
    }
    // ***
    if (ldap_bind ….

    That’s it.

  26. mike Says:

    hi Gonzalo,

    I use your datasource class and it works fine.
    There is just one thing I can’t get to work. I have groups with members. If I read the group and make a print_r i see the following:

    Array
    (
        [LdapGroup] => Array
            (
                [cn] => Tester
                [objectclass] => groupOfNames
                [member] => Array
                    (
                        [0] => uid=user1,ou=People,dc=mycompany,dc=com
                        [1] => uid=user2,ou=People,dc=mycompany,dc=com
                        [2] => uid=user3,ou=People,dc=mycompany,dc=com
                    )
    
                [dn] => cn=Tester,ou=Group,dc=mycompany,dc=com
                [count] => 1
            )
    
    )
    

    If I try to save a modified group back with
    $this->LdapGroup->save($this->data)
    there are no errors, but the changes are not saved

    The Array I try to save back looks like this

    Array
    (
        [LdapGroup] => Array
            (
                [cn] => Tester
                [objectclass] => groupOfNames
                [member] => Array
                    (
                        [0] => uid=user3,ou=People,dc=mycompany,dc=com
                        [1] => uid=user4,ou=People,dc=mycompany,dc=com
                        [2] => uid=user5,ou=People,dc=mycompany,dc=com
                    )
    
                [dn] => cn=Tester,ou=Group,dc=mycompany,dc=com
                [count] => 1
            )
    
    )
    

    Do you have a idea why the values are not saved?

    Cheers
    Michael

    • gservat Says:

      Hi Michael,

      I’m not sure why it doesn’t save. All I can suggest is that you look at the LDAP log to see what error it throws when you try and save. It might shed some light on what the problem is with the data it’s receiving.

      PS: Sorry about the late reply (that goes for the other few comments, too)

  27. Random Tech Articles » CakePHP with full CRUD, a living example! Says:

    […] One by euphrate, unfortunately this one was only for reading from ldap.  The second one was by Gservat, this one was a bit more complete, but was not really working for me and  as i read from his […]

  28. analogrithems Says:

    So I’ve fixed several of the issues people were having with this class and have posted an article on my site about using it. http://www.analogrithems.com/rant/2009/06/12/cakephp-with-full-crud-a-living-example/

    I’ve also written an an auth component that uses this data source http://www.analogrithems.com/rant/2009/06/13/ldapauth-component-for-cakephp/

    They both work with current cake 1.2.3.8116

    • gservat Says:

      Good work! Thanks for improving it!

      • Analogrithems Says:

        Hey I’m trying to get this datasource added to cakephp source tree, they wont accept gpl, only MIT. Do you care if the license is changed? I’ve made several improvements to the code and would like to get others testing and giving feedback to make it better. I’m hoping this will also get the ball rolling on adding bas and belongs to relation functions. Email me and we can collaborate further.

  29. analogrithems Says:

    Hey Michael,
    The error you are getting is because the dn & count are getting sent in your update. This causes and object Class violation.. I’ve corrected this in the updated ldap datasource. As well as added detailed logging to a seperate ldap.error file when ever an ldap action fails to see specifically what was sent to the ldap server.

    Once again major thanks to Gservat for getting me started on this

Leave a reply to gservat Cancel reply