PHP LDAP: Get All Active Directory Groups and other useful things….

As the suite of applications and other useful tools I’m developing for our organization expand,   I’ve found it increasingly necessary to integrate with our Active Directory.

With the assistance of our AD guru, Adam Witt,  and Professor Google,  I’ve finally managed to pull the information I need,  without breaking anything.

My goal is to maintain a database table of AD groups in order to control access to certain applications.

If you’re sitting there thinking that I’m sort of sort of idiot because I’m not pulling the AD groups as needed,  I explain all that at the end of this blog post….please read it before pointing out what is blindingly obvious.

I used the PHP LDAP library for this project.  Here is a link to the entry on the PHP site.   Please note the prerequisites.

This is how to connect to the LDAP server


$ldap = ldap_connect('867.530.09.00','389');  //its not a real IP....its a musical easter egg!
if (!$ldap) {
die('No LDAP');
exit;
}

That’s the easy part.

In order to successfully pull information from your Active Directory,  it is essential that you know how its laid out.

Note:  I specified to KNOW how its laid out,  not how you THINK its laid out.

(in other words:  learn from my experience!)

Using the Microsoft AD Browser helped me tie everything together,  and figure out exactly what I had to use as filters, and search strings.

Before I get to the big chunk of code that does things,  I’m going to cover each of the gotchas that I had to work past.

LDAP Operations Errors

After much flailing about I found that there were two things causing these.

Remember to set Options

ldap_set_option($ldap,LDAP_OPT_REFERRALS,0);
ldap_set_option($ldap,LDAP_OPT_PROTOCOL_VERSION,3);

If your AD administrator likes things like security,  you will have to use the ldap_bind command to register with the AD server!

Here’s the code to tell the AD server the credentials of your PHP script


ldap_bind($ldap,'xxxxxxx','thisisnotreallyapassword');

So now you can connect to your AD server,  AND your AD server knows what rights and privileges your PHP script has.

The next step is to tell the PHP script where to look for whatever it is you want to look for.

Here’s the command:


$result = ldap_search($ldap,$base_dn,$filter,$attributes);

The parameters can be a bit tricky,  and this is where the understanding your AD layout is crucial.

The $ldap parameter is your connection that you set up earlier.

The $base_dn is the Distinguished Name at the top of the part of the AD structure you want to search.

Note that the base_dn is NOT limited to DN’s,  you can add other attributes.

In this example,  I’m going to want to search within the OU (Organizational Unit) of STAFF, within the DCs (Domain Controller)  of MAIN,  SITE, CA.

Loading the $base_dn variable will look like this:

$base_dn = "OU=STAFF,DC=MAIN,DC=SITE,DC=CA";

I will confess that I haven’t spent a lot of time playing with filters and attributes.   I’ve seen some examples of fairly complex filters,  so if you’re looking for something like that,  you can always hit up Google.   If you find good, working examples, I’d invite you to post them in the comments here.

For my example,  we’re going to look for all “OU”, “CN”, and “DC” attributes within any child of our base that is an OU.

$filter = "(OU=*)";

$attr = array("OU","CN","DC");

$result = ldap_search($ldap,$base_dn,$filter,$attr);

$rescount = ldap_count_entries($ldap,$result);

$data = ldap_get_entries($ldap,$result);

echo '<pre>';

foreach($data as $row) {

print_r($row);

}

That’s a pretty basic example.  I don’t have any sample data to show you as that would require either sending you our actual AD structure (which I think would violate some terms of my employment contract),  or require me to go and build an AD server.

I will leave you with this bit of code that I used to examine and validate the information that I was getting.

I wanted to ensure that I was only working with groups that had users,  so, in my validation stage I used this bit of code:


foreach ($info as $k=>$v) { //$v['dn'] becomes my base domain as I want to search its children
$sres = ldap_search($ldap,$v['dn'],"(CN=*)",$attrib);

if (ldap_count_entries($ldap, $sres) > 0 ) {

echo $v['dn'].' has '.ldap_count_entries($ldap, $sres).' users<br>';

}

echo '<hr>';
}

Hopefully it helps.  If you’ve any questions or improvements,  please feel free to post them.

Now…for those of you who can’t think of any reason why I wouldn’t just pull AD groups from the server as users connected….

Yes…I know that I could just pull the AD groups on the fly (I’m not stupid),     I want to ensure that I can maintain security functionality in the event that the AD server isn’t available (or doesn’t respond fast enough),   and,  I want the ability to audit access,  meaning that I would want to have a record of a user’s AD groups at the time they logged in.

Just because I know some of you are thinking about how stupid I am to not just record the groups in some sort of audit table,  I’m going to explain why I’m doing that way.

The short answer is Database Normalization.   

Database Normalization refers to the practice of designing a database to avoid, as much as possible, storing redundant data.   Meaning that if you have a piece of information,  such as an address,  that would be associated to another entity (such as a person),  that you record that piece of information once,  and then associate it to the parent entities through a joining table.

I know some people have a hard time wrapping their heads around that,  but believe me,  its an actual thing.

In this specific case,  recording a user’s groups (as each would have several) each time they connect would create a row for each group,  for each user,  each time they logged in.

When I was testing the first part of this project,   I connected twice and created 16 rows…..8 rows twice.

Imagine how fast that audit table would grow with 100’s of users connecting multiple times a day.

So,  when tracking data like this for historical audit purposes,  I keep a table of all AD groups.   Each group,  in addition to having its name stored in the table,   it will also have a unique identifier field that would serve as this table’s primary key.

With this solution,  instead of having 16 rows with the name of the group,  I would have 16 rows of at least two columns.

Column 1 would contain the unique identifier of my row in the user table,   and Column 2 would contain the unique identifier of the row of the groups table containing the individual group.

This solution takes up much less space,  and,  with proper indexing,   makes database operations much faster and more efficient.

There is also the added bonus that other database programmers won’t make fun of me.