CakePHP with full CRUD, a living example!

Jun 12th
Posted by analogrithems  as CakePHP, ldap

I’ve been using CakePHP for a while now and I’ve been thinking for a while it was time to see if I could give something back. As an IT leader I’m in love with LDAP. It makes life so simple for me and my team. The big downside to LDAP is it’s not very easy to learn how all the objectClasses and attributes work with various applications. Microsoft has eliminated this with the Microsoft Management Console (MMC). It amazes me that no open source project has developed a tools such as this before. I’ve worked on a open source tool in the past that was a web interface wrapper around ldap to do account management, so I’m familiar with the requirements such an application should have. Being that CakePHP is so powerful, I wanted to see if I could do this with that.

When I first started, I realized that CakePHP didn’t have an LDAP data source officially supported yet. I did find two articles about some good attempts. 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 comments many others. I think we wrote his for CakePHP 1.1. Since I wanted to use Current cake 1.2.8xxx I set out to use this as my start and fix/extend it.

Before we get started I want to state the environment I was using to do my work was Redhat Enterprise 5.2 & Fedora 10 (Work requirement) with redhat directory server 8.1 and Fedora directory server 1.2. Now while LDAP is a standard protocol, some of the driver may have become centric to those platforms, so if this is the case, please leave me a comment and I will try to correct the ldap data source I’m working on. My hope is to get ldap as an offical CakePHP data source. With that said the reason i call this a living example is because I’ve continued to upgrade and improve this data source as well as this article. Some of the next features I want to implement is data associations. Basically has and belongs to many relations. This way when you look up an user account it also shows you all the groups that user is in. This will take some time but I’ll get there. This work is all being done in the hopes that I can use this data source and CakePHP to build a really user friendly web interface for managing enterprise LDAP infrastructures without a whole lot of LDAP knowledge.

8/20/2009 – New Home for the source. I’ve got this datasource in my github tree now http://github.com/analogrithems/idbroker/tree/master/models/datasources enjoy, and feel free to submit bugs or request there.

7/13/2009 – updated ldap_source.php to make better use of the debug describe code. Also fixed the way things update. Only update what has changed instead of whole record. This will help with LDAP aci rules when logging in as non-admin users and trying to do things like update your userpassword or email.

6/20/2009 – Updated ldap_source.php to work with OpenLDAP 2.3 schema system. First it will try ‘cn=schema’, if that doesn’t return any results then it looks for schemas in ‘cn=subschema’ this make sure the code will work with OpenLDAP as well as the Netscape based versions like iPlanet, Redhat Directory Server, Fedora Directory Server etc.

First things first, here is my ldap data source for CakePHP. You will need to download this ldap_source.php to your ‘app/models/datasources/’ directory.

So lets dive right in below is the database config we will use.

<?php
class DATABASE_CONFIG {
 
	// if using ssl set 'host' => ldaps://hostname and 'port' => 636
        // If using tls set 'tls' => true and 'port' => 389
 
	var $ldap = array (
		'datasource' => 'ldap',
		'host' => 'localhost',                
		'port' => 389,                        
		'basedn' => 'dc=examnple,dc=com',
		'login' => '', 
		'password' => '',                
		'database' => '',
                'tls'         => false,
		'version' => 3                    
	);     
}
?>

You notice that the variables database, login and password are blank. Keep at least database this way. You can populate login and password if don’t want your ldap connections to be anonymous. I keep mine blank because I have written my own auth component that uses ldap, So once I’m authed that gets passed to the datasource instead. This is a ugly hack that I’ve written another post about.

Here is our people model for accessing the users in your LDAP tree.

 <?php 
class People extends AppModel {
 
	var $name = 'People';
 
	var $useDbConfig = 'ldap';
 
	// This would be the ldap equivalent to a primary key if your dn is 
	// in the format of uid=username, ou=people, dc=example, dc=com
	var $primaryKey = 'uid';     
 
	// The table would be the branch of your basedn that you defined in 
	// the database config
	var $useTable = 'ou=people'; 
 
	var $validate = array(
		'cn' => array(
			'alphaNumeric' => array(
				'rule' => array('custom', "/^[a-zA-Z]*$/"),
				'required' => true,
				'on' => 'create',
				'message' => 'Only Letters and Numbers can be used for Display Name.'
			),
			'between' => array(
				'rule' => array('between', 5, 15),
				'on' => 'create',
				'message' => 'Between 5 to 15 characters'
			)
        ),
        'sn' => array(
			'rule' => array('custom', "/^[a-zA-Z]*$/"),
			'required' => true,
			'on' => 'create',
			'message' => 'Only Letters and Numbers can be used for Last Name.'
        ),
        'userpassword' => array(
			'rule' => array('minLength', '8'),
			'message' => 'Mimimum 8 characters long.'
        ),
        'email' => array(
			'rule' => 'email',
			'required' => true,
			'on' => 'create',
			'message' => 'Must Contain a Valid Email Address.'
		),
        'uid' => array(
			'rule' => 'alphaNumeric',
			'required' => true,
			'on' => 'create',
			'message' => 'Only Letters and Numbers can be used for Username.'
        ),
    );
 
 
}
?>

Here is a very basic controller to accompany our people model. It demonstrates the important core functions and should get you started on using this data source with your own application.

<?php
class PeoplesController extends AppController {
 
	var $name = 'Peoples';    
	var $components = array('RequestHandler');
	var $helpers = array('Form','Html','Javascript', 'Ajax');
 
 
	function add(){
            if(!empty($this->data)){
			$this->data['objectclass'] = array('top', 'organizationalperson', 'inetorgperson','person','posixaccount','shadowaccount');
 
			if($this->data['password'] == $this->data['password_confirm']){
				$this->data['userpassword'] = $this->data['password'];
				unset($this->data['password']);
				unset($this->data['password_confirm']);
 
				if(!isset($this->data['homedirectory'])&& isset($this->data['uid'])){
					$this->data['homedirectory'] = '/home/'.$this->data['uid'];
				}
 
				if ($this->People->save($this->data)) {
					$this->Session->setFlash('People Was Successfully Created.');
					$id = $this->People->id;
					$this->redirect(array('action' => 'view', 'id'=> $id));
				}else{
					$this->Session->setFlash("People couldn't be created.");
				}
			}else{
				$this->Session->setFlash("Passwords don't match.");
			}
                }
		$this->layout = 'people';
	}
 
	function view( $id ){
		if(!empty($id)){
			$filter = $this->People->primaryKey."=".$id;
			$people = $this->People->find('first', array( 'conditions'=>$filter));
			$this->set(compact('people'));
		}
		$this->layout = 'people';
	}
 
	function delete($id = null) {
		$this->People->id = $id;
		return $this->People->del($id);
	}
 
}
?>

So lets talk about somethings here, in our model we define $primaryKey & $useTable variables. The $useTable is the branch of the ldap server. For this models purpose we define our table as ‘ou=people’. This makes sure that objects we create (I.E. Users/people) will be added under the organization unit people. It also makes sure that when you pass something like ‘jdoe’ to the delete action it will search that branch for the user object to delete. The $primaryKey also helps in the creation and deleting of users. It makes sure that the dn is created as uid, this is helpful to make sure that a user doesn’t already have that user name. Also since ldap is case insensitive you don’t have to worry about the possible variations of the object names when checking the existence.

Now your model isn’t limited to one branch or object type. If you wanted to create a browser for example your could define a model like the following.

<?php 
class Browser extends AppModel {
    var $name = 'Browser';
    var $useDbConfig = 'ldap';
    var $primaryKey = 'dn';
    var $useTable = '';
}
?>

You’ll notice here we set our $useTable to nothing (important, you get errors about no db defined from CakePHP if this missing). The really interesting part here is that we set $primaryKey to dn. This is the ultimate primary key for our type or data source. The difference here is that when we create/delete an object we have to pass it the full dn.

Our new data source also adds some new options to the find function.
$options['targetDN'] : This is more like the point in the tree we want to start our search. If you don’t define it it defaults to the $useTable.$config[$useDbConfig]['basedn'] if your $useTable variable is empty it defaults to the basedn configured in your database config.

$options['scope'] : If you’ve worked with ldap before then you are familiar with the concept of search scopes. You have three search scopes, ’sub’, ‘one, & ‘base’. Basically sub means search from this point down the tree. one means search one level below this point and base means search just this point. For example if you wanted to see if a user already existed you could set the targetDn to uid=jdoe,ou=people,dc=example,dc=com and it will check if this object already exists. The default scope is sub

Tags

35 Comments

  1. egelados  21st June 2009  

    exceptional article :)

  2. jaymitt  3rd July 2009  

    Thanks for writing this!
    I originally found euphrate_ylb’s article on the cakephp site, then followed it to gservat’s, but they were a little difficult to understand because I’m a noob to cakePHP and PHP in general.

    This really helps out. I’m in IT as well, and like many companies we are Microsoft-centric, but I would rather focus my learning efforts on open-source technologies.

    That is why I’m learning/using PHP and cakePHP, and I want to write some admin web interfaces that will be able to interact and pull data from our MS Active Directory. I will be using this code to try and accomplish this – I’ll let you know how it goes.

    • analogrithems  13th July 2009  

      Awesome, if you have any troubles let me know. I’d like to make sure this works with MS as well, It fact I wrote the ldap datasource for a web based mmc for unix and hopefully windows. I really like the way the M$ mmc works but there is nothing like it for Unix yet. Also I’d like to have a web thin client version for windows to manage AD domains.

  3. dotbart  13th July 2009  

    Hi,
    thanks for both updating and sharing your source. It’s a really nice thing to do :-)

    I have a question tho. When using the old version from gservat, I didn’t get any errors connecting to the LDAP. Just when writing to it..
    However in your version I get the following when not executing anything (just opening a form in a controller wich uses the datasource):
    “Warning (2): ldap_count_entries(): supplied argument is not a valid ldap result resource [APP\models\datasources\ldap_source.php, line 577]”

    So I guess this is Cake trying to find out the Model structure like the DESCRIBE in SQL. Great, but why the error?

    In your code, I found you’re looking for cn=Schema. When searching my LDAP, I can’t find an object like that. Is that what causes it? How can I solve this?

    Thanks,
    Bart

    • analogrithems  13th July 2009  

      I’ve updated my code. The cn=schema was for pulling the schema. I originally wrote it around redhat ds. I then updated the code to use cn=schema if that fails then use cn=subschema (Openldap). I’ve also made several fixes to the debug describe format. The ldap_count_entries is ignore able and i will actually place an @ to silence it, notice it’s only a warning. This is caused by always checking for record count for compatibility to cakephp model design

  4. Brendan  17th July 2009  

    The link to ldap_source.php at the top of the article is returning a 404.

    I think this is the correct URL:

    http://www.analogrithems.com/rant/wp-content/uploads/2009/07/ldap_source.phps

    • analogrithems  20th July 2009  

      Good call, it’s updated. Sorry everyone.

  5. Robert  15th September 2009  

    Thanks for the code.
    Works great.

    I had to change line 72 in ldap_source.php:

    if (!ldap_start_tls($this->connection)) {

    $this->log(“Ldap_start_tls failed”, ‘ldap.error’);

    fatal_error(“Ldap_start_tls failed”);

    }

    Changed $this->connection in $this->database on the 1st line:

    if (!ldap_start_tls($this->database)) {

    $this->log(“Ldap_start_tls failed”, ‘ldap.error’);

    fatal_error(“Ldap_start_tls failed”);

    }

    • analogrithems  16th September 2009  

      Good eye, updated in git repo at github

  6. orangepeelbeef  12th January 2010  

    I can’t for the life of me get this to work with microsoft Active directory.
    I am sure this is related to the schema, but can’t get it working.

    2010-01-12 18:35:51 Warning: Warning (2): ldap_count_entries(): supplied argument is not a valid ldap result resource in [APP/models/datasources/ldap_source.php, line 592]
    2010-01-12 18:35:51 Warning: Warning (2): ksort() expects parameter 1 to be array, null given in [APP/models/datasources/ldap_source.php, line 1245]
    2010-01-12 18:35:51 Warning: Warning (2): Invalid argument supplied for foreach() in [CORE/cake/libs/view/helpers/form.php, line 128]
    2010-01-12 18:35:51 Notice: Notice (8): Undefined variable: schema_entries in [APP/models/datasources/ldap_source.php, line 617]
    2010-01-12 18:35:51 Notice: Notice (8): Undefined variable: return in [APP/models/datasources/ldap_source.php, line 721]

    any ideas?

    • analogrithems  13th January 2010  

      what version of windows are you using? I’ll get something setup.

  7. Bas Tichelaar  13th January 2010  

    This datasource works almost… Except I use OpenLDAP provided by Ubuntu, which has its schemas stored in the LDAP server (cn=schema,cn=config), and I cannot retrieve them using the getLDAPschema() class. Maybe you can point me in the right direction?

    • analogrithems  13th January 2010  

      what version of openldap? does the schema live at cn=scheme or cn=subscheme?

  8. orangepeelbeef  13th January 2010  

    windows server 2003. Those errors may be strictly cosmetic, I think it is actually binding to the ad server. Had some trouble with the ‘proxy user’ it ended up having to be a fully qualified cn=blabla,dc=bla etc. I was hoping username@domain.com would work, but it didn’t. So, in short, i believe it is functioning, even with those errors above.

    • analogrithems  13th January 2010  

      The way to tell is simply in your view do a

      < ?php print_r($this->data); ?>

      i believe and this will tell you all the details of your datasource connection

  9. orangepeelbeef  13th January 2010  

    Some fixes to get rid of the errors when it can’t get the schema:

    592: if(@ldap_count_entries($this->database, $check) > 0){

    617: if( isset($schema_entries) ) {

    //add before 721
    if (!isset($return)) {
    $return=array();
    }
    721: return $return;

    but with those fixes in, still getting these errors:
    Notice (8): Undefined index: attributetypes [APP/models/datasources/ldap_source.php, line 1246]
    Warning (2): ksort() expects parameter 1 to be array, null given [APP/models/datasources/ldap_source.php, line 1247]
    Warning (2): Cannot modify header information – headers already sent by (output started at /var/www/fw_request/app/models/datasources/ldap_source.php:1282) [CORE/cake/libs/controller/controller.php, line 644]

  10. orangepeelbeef  13th January 2010  

    header information was whitespace in my ldap_source.php file after the ?>

    ksort seems to be failing because the object->schema contains nothing.

    otherwise, i’m up and running.

    How do I validate that a user is already logged in?

    How do I retrieve the user’s email for example?

  11. orangepeelbeef  13th January 2010  

    function describe(&$model, $field = null){
    $attrs=null;
    $schemas = $this->__getLDAPschema();
    if (isset($schemas['attributetypes'])) {
    $attrs = $schemas['attributetypes'];
    ksort($attrs);
    }
    if(!empty($field)){
    return($attrs[strtolower($field)]);
    }else{
    return $attrs;
    }
    }

    fixes the attributetypes and ksort errors

    but again, i don’t know if the schema is necessary for retrieving info or what.

    • analogrithems  13th January 2010  

      Ya, the schema is important. It does the input validation. I’m going to try to setup a windows 2003 server this weekend so i can get the schema working under it. Once I identify the schema path and the make sure its objectclasses and attributes are parsed right it should fix your problems.

  12. orangepeelbeef  13th January 2010  

    I found this, but I’m not sure where to put it or if it’s even the right thing…

    http://wiki.bestpractical.com/view/LdapAttrMap

    Here are mappings which should work with a Windows Active Directory server (Win2000 and Win2003).

    Set($LdapAttrMap, {‘Name’ => ’sAMAccountName’,
    ‘EmailAddress’ => ‘mail’,
    ‘Organization’ => ‘physicalDeliveryOfficeName’,
    ‘RealName’ => ‘cn’,
    ‘ExternalContactInfoId’ => ‘dn’,
    ‘ExternalAuthId’ => ’sAMAccountName’,
    ‘Gecos’ => ’sAMAccountName’,
    ‘WorkPhone’ => ‘telephoneNumber’,
    ‘Address1′ => ’streetAddress’,
    ‘City’ => ‘l’,
    ‘State’ => ’st’,
    ‘Zip’ => ‘postalCode’,
    ‘Country’ => ‘co’}
    );

    • analogrithems  13th January 2010  

      No, this is from RT which is witten in Perl. I need to get the schema working for ad, that fixes the issues you are having.

  13. Bas Tichelaar  13th January 2010  

    The version of OpenLDAP is 2.4.18. More information is on this page: http://www.openldap.org/doc/admin24/slapdconf2.html. In short, the current version of the datasource retrieves the main schema (cn=schema,cn=config) but not the subschemas cn={0}core,cn=schema,cn=config for example. Thanks for your quick replies, let’s hope we get this working!

    • analogrithems  13th January 2010  

      I’l try to setup a current version of the openldap to make it work with the datasource. I need to do this with AD also.
      It’s important for it to load the schema array since it will use it later for data validation. The schema’s state which fields are must vs may. In other words it helps input validation know which fields you must have vs which fields you may have.

      in the mean time if you want to try to fix the datasource for this version of openldap look at
      http://github.com/analogrithems/idbroker/blob/master/models/datasources/ldap_source.php
      at line 589 i have the function that trys to parse the schema

  14. orangepeelbeef  13th January 2010  

    I pulled the schema naming contexts from my AD server. Here are the results. (replaced my root dc with ROOTDC to limit information exposure)

    ‘type’ => ‘namingContexts’,
    ‘vals’ => [
    ‘ROOTDC’,
    ‘CN=Configuration,ROOTDC’,
    ‘CN=Schema,CN=Configuration,ROOTDC’,
    ‘DC=DomainDnsZones,ROOTDC’,
    ‘DC=ForestDnsZones,ROOTDC’

  15. orangepeelbeef  13th January 2010  

    Also, Fwiw, phpldapadmin can retrieve my AD schema with no modifications. I only mention this because the __getLDAPschema() code mentions it was borrowed from phpldapadmin. I assume it was borrowed a long time ago, and things have probably changed dramatically.

  16. orangepeelbeef  15th January 2010  

    Aside from the schema it seems to be authenticating properly. I see the user information gets dumped into the session data, but how do I access the ldap attributes? I want to retrieve the email address the ‘mail’ attribute, but i’m not having much luck getting that info. Should I be grabbing it out of the session or is there some trick for getting it via something like $user['mail'] ?

    • analogrithems  15th January 2010  

      I do it like this
      function view( $id ){
      if(!empty($id)){
      $filter = $this->Person->primaryKey.”=”.$id;
      $people = $this->Person->find(‘first’, array( ‘conditions’=>$filter));
      $this->set(compact(‘people’));
      }
      }

      And in my model I define my primary Key like this
      // This would be the ldap equivalent to a primary key if your dn is
      // in the format of uid=username, ou=people, dc=example, dc=com
      var $primaryKey = ‘uid’;

      Note though that depending on what your trying to model you can change the primaryKey, sometimes you want to look at your objects holistically so the primaryKey would be ‘dn’. Or if you are dealing with groups your primary key would be ‘cn’. This takes a little getting used to.

      In My view I can then access the cn like this
      < ?php echo $this->data['Person']['cn']; ?>

  17. orangepeelbeef  15th January 2010  

    Thanks, that was a big help. I now can view the ldap user data within the users controller, but how do I know what the uid of the user who is currently logged in is? That is the last piece I’m missing to make this functional. I also want to do some differing access levels based on what ldap group the user is in, so I need to be able to access the ldap info for the user who previously logged in. I used beforefilters to force a login for all my pages.

  18. orangepeelbeef  15th January 2010  

    figured it out:

    controller:

    $user = $this->LdapAuth->user();
    $this->set(compact(‘user’));

    then can retrieve as

    view:
    echo $user['User']['mail']

  19. Bas Tichelaar  17th January 2010  

    Hi, any progress made already?

  20. ldibanyez  20th January 2010  

    Hi there,

    First of all, congratulations for your effort, is really useful.

    I’ve just set it up and succesfully read (thats all i need for the app i’m doing) the users, but now i need some relations between ldap-sourced models and between ldap-sourced and sql-sourced models. I’ve seen that you implemented ‘generateassociationQuery’ but i can’t figure out what goes in the ‘foreignKey’ field.

    For example, if i got a set of ou (departments), and inside each of them a set of users, how i say to cake that a user belongsTo a department?

    Or maybe i’m just screwing it all up, because foreign key is a relational concept not applicable to ldap, and a i’ll be better making a full dn find to know the users that belong to a department.

    Thanks in advance.

    • analogrithems  11th February 2010  

      The foriegn key is how you want to index your ldap data. If you have the cn and want all your lookups in a certain controller/model to be via the cn then use the cn as your primary group. This works well in ou=people or ou=group context. If you are referring to any object in the tree, or you don’t specify the foriegn key then it will be dn. The foreign key is just the index really.

      In your situation I would make my departments groupofuniquenames and follow the traditional ldap DIT (Directory information tree). Then include each users dn in that group. This make more sense in the long run also because if you are using something like apache to perhaps restrict access to a site (For example only billing department can access billing records page). Then you’ll be happy you followed the standard convention because you can tell apache only people in the group ‘cn=billing, ou=groups,dc=example,dc=org’ are allowed access. Also some people may be in multiple departments. What do you do then? In the group context it’s perfectly fine to have someone in multiple groups. From the IT perspective I’ve found I often place my IT people in each group to verify they work. This makes it easier for them to support the end users.

  21. Benedikt Trefzer  5th March 2010  

    Hi
    I managed to use your database model for a cakephp project connecting to openldap. Good work, congrats.
    But I made several changes, which I like to share with you:

    Function update:
    if((!$this->in_arrayi(‘userpassword’, $update))&& (isset($entry['userpassword']))){
    => password needs to be set, to unset. Found the problem, when updating entries without password (eg objectclass top).
    same for the following lines:
    if (isset($entry['count'])) { unset($entry['count']);}
    if (isset($entry['dn'])) { unset($entry['dn']); }

    For the return value of the function, I added a return true, if $entry is empthy (if you call update but nothing changes….)

    To make the whole story to work with openldap, I changed all occurences of ‘attributetypes’ with ‘olcattributetypes’ and all occurences of ‘objectclasses’ with ‘objectclass’.
    Then I made: $schemaDN = ‘cn=schema,cn=config’; (added cn=config).
    Probably it would be a good thing to make this hardcoded things a config option in the database definition.

    I still have problems with adding new entries, but give me some time to find out :)
    Another trouble: Paging does not work (not implemented, needs limit of data) and Sort is also not working. Could you please tell me if sort is working for you ?

    If you like my changes as a path, let me know.

    Regards Benedikt

    • analogrithems  5th March 2010  

      I’ve been trying to decide if it should be a config option, or if it should be case statement t try to discover what the ldap server type is.

Trackbacks/Pingbacks

  1. CakePHP with ACL+LDAP Project Intro « benhavilland
  2. Step 3 – Adding LDAP « benhavilland

Leave a Reply

Powered By Wordpress || Designed By Ridgey