codecrafting-io / adoldap
A PHP seamless way to search on the Active Directory with ADO and LDAP
Requires
- php-64bit: >=7.1
- ext-com_dotnet: *
Requires (Dev)
- devster/ubench: ^2.1
- phpunit/phpunit: 6.*
- symfony/var-dumper: ^4
README
Summary
⚠️ WARNING: ⚠️ This library still on alfa, so testing is on the way and newer versions may break backwards compability.
The AdoLDAP is a small PHP library to seamless search and authenticate on Active Directory with ADO and LDAP. In short terms it provides the following benefits:
- ⭐ Seamless way to connect to Active Directory, not requiring a single configurtion, not even a server if you wanted.
- ⭐ A nice semantic syntax that is easy/reusable/fun to use. You can use pre built searchs to find Users, Computers and Groups, handling them as objects, with a easy human readable get/set syntax.
- ⭐ A fluent QueryBuilder having support for both LDAP and SQL dialects.
- ⭐ Tools to discover information about you current env, regarding to the available Domain Controllers, connected DCs, main DCs, domain name etc.
- ⭐ Native PHP data type handling. No strangeous and obscure
VARIANT
objects for returned values, with a nice Iterator interface to loop through objects.
How it works
The main feature that AdoLDAP provides is a seamless way to authenticate on AD with LDAP using the current security context of the thread in execution. This is a feature implemented by the ADODB Active Directory Interface, which can be used through COM objects whithin PHP language (or any COM aware language). Usually this means if the current user are logged on a domain, and this user have permission to search on AD (which likely will), the ADODB don't require especific cridentials to connect.
In another words, now you can create web applications that can search through LDAP without the need of a especific read/write user to connect. For example, you can use Windows Authentication setup and take advantage of a seamless search information about the current authenticated user. You also can test on your local machine even not having Windows Authentication.
Requirements
- Windows environtment.
- PHP >= 7.1 64bits
- COM PHP extension enabled
- Composer
The AdoLDAP only works on Windows, because COM extensions is the way to use ADODB, since ADO is a Windows only. To install AdoLDAP use the following command:
composer require codecrafting-io/adoldap
Configuration
Domain Information
Like was said you can search providing no information to connect. This is due to the feature of ADODB that allows connections provding zero information about, credentials or host to connect. If you provide no information about the host or the baseDn to be used to connect, the library will likely connect to the user logonDomainController
, which is the domain controller host that the user was last connected.
To understant what domain informations are available, use the following code:
try { $ad = new AdoLDAP(); dump($ad->info()); } catch(\Exception $e) { dump($e); }
The table below provides the information for each attribute returned:
Connection configuration
Even not beign necessary to provide a single configuration to connect, is recommended to do it due to consistency of the search results and specially for performance reasons. Is recommeded to provide a BASE_DN (you can use the defaultNamingContext
) and for servers is preferreable to use the primaryDomainControllers
, to a faster connection. So after you inspect the values returned by the code above, use them to connect like in the code below:
try { //Minimal recommended configuration $config = [ 'host' => 'server01.mydomain.com', //use a primaryDomainController 'baseDn' => 'DC=MYDOMAIN,DC=COM', ]; $ad = new AdoLDAP($config); //auto connected } catch(\AdoLDAPException $e) { dump($e); }
The table below provides the information for each configuration:
Searching
There are two ways to search, using RAW queries or building through the QueryBuilder
.
Searching using RAW queries:
try { $config = [ 'host' => 'server01.mydomain.com', //use a primaryDomainController 'baseDn' => 'DC=MYDOMAIN,DC=COM', ]; $ad = new AdoLDAP($config); $ad->search()->query("<LDAP://server01.mydomain.com/DC=MYDOMAIN,DC=COM>;(&(objectCategory=user)(sAMAccountName=jdoe));sAMAccountName,name"); } catch(\AdoLDAPException $e) { dump($e); }
Searching using QueryBuilder
:
try { $config = [ 'host' => 'server01.mydomain.com', //use a primaryDomainController 'baseDn' => 'DC=MYDOMAIN,DC=COM', ]; $ad = new AdoLDAP($config); $ad->search()->whereEquals('objectCategory', 'user')->findBy('sAMAccountName', 'jdoe'); } catch(\AdoLDAPException $e) { dump($e); }
Query Builder
The query builder allows you to easily create queries using both LDAP and SQL Dialects. The main class AdoLDAP
provides a search
method that is a new instance of a SearchFactory
. The SearchFactory
can setup a new QueryBuilder
to construct a search. You can use the newQuery
method of SearchFactory
to build a QueryBuilder
, but you also can use any available method of QueryBuilder
due to the magic methods present on SearchFactory
, like the example below:
$ad->search()->newQuery()->whereEquals('objectCategory', 'user')->findBy('sAMAccountName', 'jdoe'); //OR $ad->search()->whereEquals('objectCategory', 'user')->findBy('sAMAccountName', 'jdoe');
The SearchFactory
also provides pre built or scoped searchs, to facilitate search users, computers and groups.
$ad->search()->users(); //EQUALS $ad->search()->whereEquals('objectCategory', 'user');
$ad->search()->user('jdoe', ['name']); //EQUALS $ad->search()->whereEquals('objectCategory', 'user')->firstBy('sAMAccountName', 'jdoe', ['name']);
Summarizing, the QueryBuilder
is compound by methods to construct the selection of attributes and conditional clausules, to search from a sepecific source, using either the LDAPDialect
or SQLDialect
. You build a SELECT
with the select
method, define the clausules with the where
methods, having the possibility to use either the LDAPDialect
or SQLDialect
. You can also change and set a specific BASE_DN
with the from
method.
$ad->search() ->select(['name']) ->from('DC=MYDOMAIN,DC=COM') ->whereEquals('objectCategory', 'user') ->orWhere('objectCategory', 'computer') ->get();
NOTE: Is not necessary to define a base dn using from, because the value provided in configuration is used by default.
Search Dialects
The QueryBuilder
can search using both the LDAP and SQL dialects. You can configure the dialect on the configuration, like the example below:
try { $config = [ 'host' => 'server01.mydomain.com', 'baseDn' => 'DC=MYDOMAIN,DC=COM', 'dialect' => SQLDialect::class ]; $ad = new AdoLDAP($config); } catch(\AdoLDAPException $e) { dump($e); }
The default dialect is LDAPDialect
. You can have your own implementation of both dialects by implementing a DialectInterface
.
Handling Data
Once you create your search, the data is retrivied by a ResultSetIterator
. Using ADODB the result data is returned by a RecordSet which have similar function as a cursor, or a iterator. The ResultSetIterator
implements the native PHP classes SeekableIterator
and Countable
, so in this way it's possible to simply loop through like it was array
on a foreach
.
$users = $ad->search()->users() ->select(['sAMAccountName', 'name', 'thumbnailPhoto', 'mail']) ->whereMemberOf('CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM')->get(); foreach($users as $user) { echo $user->sAMAccountName . '<br>'; echo $user->name . '<br>'; echo $user->thumbnailPhoto . '<br>'; echo $user->mail . '<br>'; }
Each position of a RecordSet is retrievied by the current
method of a ResultSetIterator
. The data is typically returned as a Entry
. A Entry
holds all attributes returned by the search and expose them as get/set magic attributes, like in the example above, or you can also use getAttribute
and setAttribute
to get the desired values. If a attribute does not exists a null
value will be returned instead.
Models & Column Map Attributes
Most of the searchs actually returns one of the Models
, could beign a User
, Computer
or Group
. The Model
extends a Entry
by provinding a human readeable get/set methods for the "default attributes", that enhances and facilitates the handling of certain values. If you find particular hard to understand the meaning or just don't known the available main attributes for objects on LDAP, the Model
provides a COLUMN_MAP
that maps the most important attributes of the corresponding AD object, to the more "human readeable" names. For example take a look to the COLUMN_MAP
of a User
:
const COLUMN_MAP = [ 'objectclass' => 'objectclass', 'dn' => 'distinguishedname', 'account' => 'samaccountname', 'firtname' => 'givenname', 'name' => 'name', 'workstations' => 'userworkstations', 'mail' => 'mail', 'jobtitle' => 'description', 'jobrole' => 'title', 'address' => [ 'street', 'postalcode', 'st', 'l', 'co' ], 'mailboxes' => 'msexchdelegatelistbl', 'mobile' => 'mobile', 'phone' => 'telephoneNumber', 'department' => 'department', 'departmentcode' => 'extensionAttribute1', 'memberOf' => 'memberOf', 'company' => 'company', 'photo' => 'thumbnailphoto', 'passwordlastset' => 'pwdlastset', 'passworderrorcount' => 'badpwdcount', 'passworderrortime' => 'badpasswordtime', 'lastlogin' => 'lastlogontimestamp', 'lockouttime' => 'lockouttime', 'createdat' => 'whencreated', 'objectguid' => 'objectguid', 'objectsid' => 'objectsid' ];
So in this way, the User
model provides a list of get/set attributes using the mapped names, but if you prefer to use the original name scheme, you can still use the get/set magic attributes or the getAttribute
and setAttribute
methods.
$users = $ad->search()->users() ->select(['sAMAccountName', 'name', 'thumbnailPhoto', 'mail']) ->whereMemberOf('CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM')->get(); foreach($users as $user) { echo $user->getAccount(); //EQUALS echo $user->sAMAccountName; }
The available mapped attributes are physically present on the Model
class to facilitate the auto complete features of IDEs. The Model
can also provides special treatment to facilitate the usage of certains attributes, like a user photo.
$users = $ad->search()->users() ->select(['sAMAccountName', 'name', 'thumbnailPhoto']) ->whereMemberOf('CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM')->get(); foreach($users as $user) { echo $user->getAccount() . '<br>'; echo $user->getName() . '<br>'; echo $user->getHtmlPhoto(['class' => 'profile-picture']) . '<br>'; }
The getHtmlPhoto
returns a IMG
HTML tag, already containing the class profile-picture
, using the src
as base64 string representation of the image. For the photo attribute you can also get just the base64 or the raw binary string or even save the photo to a file, with the respective methods getPhoto
, getRawPhoto
, savePhoto
.
You can also use the mapped attributes in a QueryBuilder
selection.
$attributes = SearchFactory::translateAttributes(User::COLUMN_MAP, ['accountName', 'name', 'photo', 'mailboxes']); $ad->search()->users() ->select($attributes) ->whereMemberOf('CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM')->get(); }
If you use the SearchFactory
to search a single Entry
, the user
, computer
and group
methods also provides a parameter that already translate the attributes, in fact the default behavior of these methods is to translate attributes.
$ad->search()->user('jdoe', ['accountName', 'name', 'photo', 'mailboxes']); //EQUALS $ad->search()->user('jdoe', ['sAMAccountName', 'name', 'thumbnailPhoto', 'msExchDelegateListBl'], false);
If you provide no attributes to those methods, the entire COLUMN_MAP
will be used instead.
$ad->search()->user('jdoe'); //EQUALS $ad->search()->user('jdoe', User::getDefaultAttributes(), false);
⚠️ IMPORTANT: ⚠️ Is possible to select all attributes, by provinding the value
['*']
, but this is EXTREMELY DISCOURAGED, not only by the fact that may have a lot of attributes that possibily won't be used, but specially to performance reasons. When you use a query, using the wildcard*
the ADODB returns the ADSPATH, which is the full distinguished name of the object, forcing the library to resolve them by binding directly to the object usingCOM
, which is EXTREMELY slow even for a single object. Normally searching for a entry takes arround 200-400ms, but binding to the object can take 4s. So only use for test purposes.
Special Attributes
In addition to the Model
, some attributes are handled as objects, like the DistinguishedName
, OS
, Address
and ObjectClass
.
DistinguidedName
The DistinguishedName
class as the name says handles the distinguished name, providing easy way to extract components and parts inside the name.
$dn = new DistinguishedName('CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM'); echo $dn->getName() . '<br>'; //Container Name - AWESOME GROUP echo $dn->getPath() //Whole Path - CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM
ObjectClass
The ObjectClass
is a simple object wrap to the array
of the objectClass
attributes. The class also provides the method getMostRelevant
which is the last and most significative name of a ObjectClass
. Each Model
already have a defined objectClass
that can be obtained by using the static method objectClass
.
echo User::objectClass()->getMostRelevant() //outputs user
Address
The Address
is a simple POJO object for the all address related attributes present on User
entry. You can note that the COLUMN_MAP
maps the address
as 4 other attributes, which are flatten to compose the selection on the QueryBuilder
. The Address also have a more clear syntax and name scheme.
$user = $ad->search()->user('jdoe', ['accountName', 'address']); //EQUALS TO: $ad->search()->user('jdoe', ['sAMAcountName', 'street', 'postalCode', 'st', 'l', 'co']); echo $user->getAddress()->getCountry();
OS
The OS
handles the OS related values of the attributes operatingSystem
and operatingSystemVersion
, that can be found on Computer
entries. The class extracts and splits the data providing methods to retrieve the name
, version
, flavour
, and also ways to easily compare OSs.
$computer = $ad->search()->computer('MACHINE01'); echo $computer->getOS()->getName(); $computer->compareTo($ad->search()->computer('MACHEINE02')) //outputs -1, 0, 1;
Paging Data
The ADODB natively page the results by using the RecordSet
, which are managed by the ResultSetIterator
. You can provide a specific LDAP page size by a value for pageSize
configuration. The default value is 1000. Nevertheless, the ResultSetIterator
provides a separate paging using the getEntries
method, which allows you to define a proper limit/offset.
//Get 10 users offseting 1 page. Returns a array of User objects $users = $ad->search()->users() ->select(User::getDefaultAttributes()) ->whereMemberOf('CN=AWESOME GROUP,DC=MYDOMAIN,DC=COM') ->get()->getEntries(10, 1);
After Fetch Callback
The ResultSetIterator
also provides a afterFetch
callback, which allows you to transform the entries every time the current
method is called.
$user = $ad->search()->user('jdoe')->afterFetch(function($user) { return [ 'name' => $user->getName(), 'accountName' => $user->getAccount(), 'photo' => $user->getPhoto() ]; })->getEntries();
You can set afterFetch multiple times, which will transform the data multiple times by the order that was provided.
Comming Soon
- Active Record
Model
. For now only search functionality is available. - Event support.
- More robust
QueryBuilder
. - Full and complete documentation site.
- Cache support.
For Last
Thanks for the Adldap2 for inspiration to create this library.
Made with ❤️ by @lucasmarotta.