52d4fabe3dce3013810cbe9765fe330fb5a19538
[timetracker.git] / WEB-INF / lib / auth / Auth_ldap.class.php
1 <?php
2 // +----------------------------------------------------------------------+
3 // | Anuko Time Tracker
4 // +----------------------------------------------------------------------+
5 // | Copyright (c) Anuko International Ltd. (https://www.anuko.com)
6 // +----------------------------------------------------------------------+
7 // | LIBERAL FREEWARE LICENSE: This source code document may be used
8 // | by anyone for any purpose, and freely redistributed alone or in
9 // | combination with other software, provided that the license is obeyed.
10 // |
11 // | There are only two ways to violate the license:
12 // |
13 // | 1. To redistribute this code in source form, with the copyright
14 // |    notice or license removed or altered. (Distributing in compiled
15 // |    forms without embedded copyright notices is permitted).
16 // |
17 // | 2. To redistribute modified versions of this code in *any* form
18 // |    that bears insufficient indications that the modifications are
19 // |    not the work of the original author(s).
20 // |
21 // | This license applies to this document only, not any other software
22 // | that it may be combined with.
23 // |
24 // +----------------------------------------------------------------------+
25 // | Contributors:
26 // | https://www.anuko.com/content/time_tracker/open_source/credits.htm
27 // +----------------------------------------------------------------------+
28
29 // NOTES:
30 //
31 // Auth_ldap.class.php was originally written for LDAP authentication with Windows Active Directory.
32 // It June 2011, it was extended to include support for OpenLDAP. The difference in the code is in the format
33 // of user identification that we pass to ldap_bind().
34 //
35 // Windows AD accepts username@domain.com while OpenLDAP needs something like "uid=username,ou=people,dc=domain,dc=com".
36 // Therefore, some branching in the code.
37 //
38 // In April 2012, a previously mandatory search for group membership was put in a conditional block (if ($member_of) -
39 // when mandatory membership in groups is actually defined in config.php).
40 // This made the module work with Sun Directory Server when NO GROUP MEMBERSHIP is specified.
41 // Note 1: search is likely to fail with Sun DS if 'member_of' => array()); is used in config.php.
42 // Note 2: search is likely to not work properly with OpenLDAP as well because of Windows specific filtering code in there
43 // (we are looking for matches for Windows-specific samaccountname property). Search needs to be redone during the next
44 // refactoring effort.
45
46
47 /**
48 * Auth_ldap class to authenticate users against an LDAP server (Windows AD, OpenLDAP, and others).
49 * @package TimeTracker
50 */
51 class Auth_ldap extends Auth {
52   var $params;
53
54   function __construct($params)
55   {
56     global $smarty;
57     $this->params = $params;
58     $smarty->assign('Auth_ldap_params', $this->params);
59   }
60
61   function ldap_escape($str){
62     $illegal = array("(", ")", "#");
63     $legal = array();
64     foreach ($illegal as $id => $char) {
65       $legal[$id] = "\\".$char;
66     }
67     $str = str_replace($illegal, $legal, $str); //replace them
68     return $str;
69   }
70
71   /**
72    * Authenticate user against LDAP server.
73    *
74    * @param string $login
75    * @param string $password
76    * @return mixed
77    */
78   function authenticate($login, $password)
79   {
80     // Special handling for admin@localhost - authenticate against db, not ldap.
81     // It is a fallback mechanism when admin account in LDAP directory does not exist or is misconfigured.
82     if ($login == 'admin@localhost') {
83         import('auth.Auth_db');
84         return Auth_db::authenticate($login, $password);
85     }
86
87     if (!function_exists('ldap_bind')) {
88       die ('php_ldap extension not loaded!');
89     }
90
91     if (empty($this->params['server']) || empty($this->params['base_dn'])) {
92       die('You must set server and base_dn in AUTH_MODULE_PARAMS in config.php');
93     }
94
95     $member_of = @$this->params['member_of'];
96
97     $lc = ldap_connect($this->params['server']);
98
99     if (isTrue('AUTH_DEBUG')) {
100       echo '<br />';
101       echo '$lc='; var_dump($lc); echo '<br />';
102       echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
103     }
104
105     if (!$lc) return false;
106
107     ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, 3);
108     ldap_set_option($lc, LDAP_OPT_REFERRALS, 0);
109     if (isTrue('AUTH_DEBUG')) {
110       ldap_set_option($lc, LDAP_OPT_DEBUG_LEVEL, 7);
111     }
112
113     // We need to handle Windows AD and OpenLDAP differently.
114     if ($this->params['type'] == 'ad') {
115
116       // Check if user specified full login.
117       if (strpos($login, '@') === false) {
118         // Append default domain.
119         $login .= '@' . $this->params['default_domain'];
120       }
121
122       if (isTrue('AUTH_DEBUG')) {
123         echo '$login='; var_dump($login); echo '<br />';
124       }
125
126       $lb = @ldap_bind($lc, $login, $password);
127
128       if (isTrue('AUTH_DEBUG')) {
129         echo '$lb='; var_dump($lb); echo '<br />';
130         echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
131       }
132
133       if (!$lb) {
134         ldap_unbind($lc);
135         return false;
136       }
137
138       if ($member_of) {
139         // Get groups the user is a member of from AD LDAP server.
140
141         $filter = 'userPrincipalName='.Auth_ldap::ldap_escape($login);
142         $fields = array('memberof');
143         $sr = @ldap_search($lc, $this->params['base_dn'], $filter, $fields);
144
145         if (isTrue('AUTH_DEBUG')) {
146           echo '$sr='; var_dump($sr); echo '<br />';
147           echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
148         }
149
150         if (!$sr) {
151           ldap_unbind($lc);
152           return false;
153         }
154
155         $entries = @ldap_get_entries($lc, $sr);
156
157         if (isTrue('AUTH_DEBUG')) {
158           echo '$entries='; var_dump($entries); echo '<br />';
159           echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
160         }
161
162         if ($entries === false) {
163           ldap_unbind($lc);
164           return false;
165         }
166
167         $groups = array();
168
169         // Extract group names. Assume the groups are in format: CN=<group_name>,...
170         for ($i = 0; $i < @$entries[0]['memberof']['count']; $i++) {
171           $grp = $entries[0]['memberof'][$i];
172           $grp_fields = explode(',', $grp);
173           $groups[] = substr($grp_fields[0], 3);
174         }
175
176         if (isTrue('AUTH_DEBUG')) {
177           echo '$member_of'; var_dump($member_of); echo '<br />';
178         };
179
180         // Check for group membership.
181         foreach ($member_of as $check_grp) {
182           if (!in_array($check_grp, $groups)) {
183             ldap_unbind($lc);
184             return false;
185           }
186         }
187       }
188
189       ldap_unbind($lc);
190       return array('login' => $login, 'data' => $entries, 'member_of' => $groups);
191     }
192
193     if ($this->params['type'] == 'openldap') {
194
195       // Assuming OpenLDAP server.
196       $login_oldap = 'uid='.$login.','.$this->params['base_dn'];
197
198       if (isTrue('AUTH_DEBUG')) {
199         echo '$login_oldap='; var_dump($login_oldap); echo '<br />';
200       }
201
202       // check if the user specified full login
203       if (strpos($login, '@') === false) {
204         // append default domain
205         $login .= '@' . $this->params['default_domain'];
206       }
207
208       $lb = @ldap_bind($lc, $login_oldap, $password);
209
210       if (isTrue('AUTH_DEBUG')) {
211         echo '$lb='; var_dump($lb); echo '<br />';
212         echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
213       }
214
215       if (!$lb) {
216         ldap_unbind($lc);
217         return false;
218       }
219
220       if ($member_of) {
221         // TODO: Fix this for OpenLDAP, as samaccountname has nothing to do with it.
222         // get groups
223
224         $filter = 'samaccountname='.Auth_ldap::ldap_escape($login_oldap);
225         $fields = array('samaccountname', 'mail', 'memberof', 'department', 'displayname', 'telephonenumber', 'primarygroupid');
226         $sr = @ldap_search($lc, $this->params['base_dn'], $filter, $fields);
227
228         if (isTrue('AUTH_DEBUG')) {
229           echo '$sr='; var_dump($sr); echo '<br />';
230           echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
231         }
232
233         // if search failed it's likely that account is disabled
234         if (!$sr) {
235           ldap_unbind($lc);
236           return false;
237         }
238
239         $entries = @ldap_get_entries($lc, $sr);
240
241         if (isTrue('AUTH_DEBUG')) {
242           echo '$entries='; var_dump($entries); echo '<br />';
243           echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
244         }
245
246         if ($entries === false) {
247           ldap_unbind($lc);
248           return false;
249         }
250
251         $groups = array();
252
253         // extract group names from
254         // assuming the groups are in format: CN=<group_name>,...
255         for ($i = 0; $i < @$entries[0]['memberof']['count']; $i++) {
256           $grp = $entries[0]['memberof'][$i];
257           $grp_fields = explode(',', $grp);
258           $groups[] = substr($grp_fields[0], 3);
259         }
260
261         if (isTrue('AUTH_DEBUG')) {
262           echo '$member_of'; var_dump($member_of); echo '<br />';
263         }
264
265         // check for group membership
266         foreach ($member_of as $check_grp) {
267           if (!in_array($check_grp, $groups)) {
268             ldap_unbind($lc);
269             return false;
270           }
271         }
272       }
273
274       ldap_unbind($lc);
275
276       return array('login' => $login, 'data' => $entries, 'member_of' => $groups);
277     }
278
279     // Server type is neither 'ad' or 'openldap'.
280     return false;
281   }
282
283   function isPasswordExternal() {
284     return true;
285   }
286 }