Refactored ActionErrors.class.php.
[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     $this->params = $params;
57     if (isset($GLOBALS['smarty'])) {
58       $GLOBALS['smarty']->assign('Auth_ldap_params', $this->params);
59     }
60   }
61
62   function ldap_escape($str){
63     $illegal = array("(", ")", "#");
64     $legal = array();
65     foreach ($illegal as $id => $char) {
66       $legal[$id] = "\\".$char;
67     }
68     $str = str_replace($illegal, $legal,$str); //replace them
69     return $str;
70   }
71
72   /**
73    * Authenticate user against LDAP server.
74    *
75    * @param string $login
76    * @param string $password
77    * @return mixed
78    */
79   function authenticate($login, $password)
80   {
81     // Special handling for admin@localhost - authenticate against db, not ldap.
82     // It is a fallback mechanism when admin account in LDAP directory does not exist or is misconfigured.
83     if ($login == 'admin@localhost') {
84         import('auth.Auth_db');
85         return Auth_db::authenticate($login, $password);
86     }
87
88     if (!function_exists('ldap_bind')) {
89       die ('php_ldap extension not loaded!');
90     }
91
92     if (empty($this->params['server']) || empty($this->params['base_dn'])) {
93       die('You must set server and base_dn in AUTH_MODULE_PARAMS in config.php');
94     }
95
96     $member_of = @$this->params['member_of'];
97
98     $lc = ldap_connect($this->params['server']);
99
100     if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
101       echo '<br />';
102       echo '$lc='; var_dump($lc); echo '<br />';
103       echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
104     }
105
106     if (!$lc) return false;
107
108     ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, 3);
109     ldap_set_option($lc, LDAP_OPT_REFERRALS, 0);
110     if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
111       ldap_set_option($lc, LDAP_OPT_DEBUG_LEVEL, 7);
112     }
113
114     // We need to handle Windows AD and OpenLDAP differently.
115     if ($this->params['type'] != 'openldap') {
116
117       // check if the user specified full login
118       if (strpos($login, '@') === false) {
119         // append default domain
120         $login .= '@' . $this->params['default_domain'];
121       }
122
123
124       if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
125         echo '$login='; var_dump($login); echo '<br />';
126       }
127
128       $lb = @ldap_bind($lc, $login, $password);
129
130       if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
131         echo '$lb='; var_dump($lb); echo '<br />';
132         echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
133       }
134
135       if (!$lb) {
136         ldap_unbind($lc);
137         return false;
138       }
139
140       if ($member_of) {
141         // get groups
142
143         $filter = 'samaccountname='.Auth_ldap::ldap_escape($login);
144         $fields = array('samaccountname', 'mail', 'memberof', 'department', 'displayname', 'telephonenumber', 'primarygroupid');
145         $sr = @ldap_search($lc, $this->params['base_dn'], $filter, $fields);
146
147         if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
148           echo '$sr='; var_dump($sr); echo '<br />';
149           echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
150         }
151
152         // if search failed it's likely that account is disabled
153         if (!$sr) {
154           ldap_unbind($lc);
155           return false;
156         }
157
158         $entries = @ldap_get_entries($lc, $sr);
159
160         if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
161           echo '$entries='; var_dump($entries); echo '<br />';
162           echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
163         }
164
165         if ($entries === false) {
166           ldap_unbind($lc);
167           return false;
168         }
169
170         $groups = array();
171
172         // extract group names from
173         // assuming the groups are in format: CN=<group_name>,...
174         for ($i = 0; $i < @$entries[0]['memberof']['count']; $i++) {
175           $grp = $entries[0]['memberof'][$i];
176           $grp_fields = explode(',', $grp);
177           $groups[] = substr($grp_fields[0], 3);
178         }
179
180         if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
181           echo '$member_of'; var_dump($member_of); echo '<br />';
182         };
183
184         // check for group membership
185             foreach ($member_of as $check_grp) {
186           if (!in_array($check_grp, $groups)) {
187             ldap_unbind($lc);
188             return false;
189           }
190         }
191       }
192
193       ldap_unbind($lc);
194
195       return array('login' => $login, 'data' => $entries, 'member_of' => $groups);
196     } else {
197
198       // Assuming OpenLDAP server.
199       $login_oldap = 'uid='.$login.','.$this->params['base_dn'];
200
201       if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
202         echo '$login_oldap='; var_dump($login_oldap); echo '<br />';
203       }
204
205       // check if the user specified full login
206       if (strpos($login, '@') === false) {
207         // append default domain
208         $login .= '@' . $this->params['default_domain'];
209       }
210
211       $lb = @ldap_bind($lc, $login_oldap, $password);
212
213       if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
214         echo '$lb='; var_dump($lb); echo '<br />';
215         echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
216       }
217
218       if (!$lb) {
219         ldap_unbind($lc);
220         return false;
221       }
222
223       if ($member_of) {
224         // get groups
225
226         $filter = 'samaccountname='.Auth_ldap::ldap_escape($login_oldap);
227         $fields = array('samaccountname', 'mail', 'memberof', 'department', 'displayname', 'telephonenumber', 'primarygroupid');
228         $sr = @ldap_search($lc, $this->params['base_dn'], $filter, $fields);
229
230         if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
231           echo '$sr='; var_dump($sr); echo '<br />';
232           echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
233         }
234
235         // if search failed it's likely that account is disabled
236         if (!$sr) {
237           ldap_unbind($lc);
238           return false;
239         }
240
241         $entries = @ldap_get_entries($lc, $sr);
242
243         if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
244           echo '$entries='; var_dump($entries); echo '<br />';
245           echo 'ldap_error()='; echo ldap_error($lc); echo '<br />';
246         }
247
248         if ($entries === false) {
249           ldap_unbind($lc);
250           return false;
251         }
252
253         $groups = array();
254
255         // extract group names from
256         // assuming the groups are in format: CN=<group_name>,...
257         for ($i = 0; $i < @$entries[0]['memberof']['count']; $i++) {
258           $grp = $entries[0]['memberof'][$i];
259           $grp_fields = explode(',', $grp);
260           $groups[] = substr($grp_fields[0], 3);
261         }
262
263         if (defined('AUTH_DEBUG') && isTrue(AUTH_DEBUG)) {
264           echo '$member_of'; var_dump($member_of); echo '<br />';
265         }
266
267         // check for group membership
268         foreach ($member_of as $check_grp) {
269           if (!in_array($check_grp, $groups)) {
270             ldap_unbind($lc);
271             return false;
272           }
273         }
274       }
275
276       ldap_unbind($lc);
277
278       return array('login' => $login, 'data' => $entries, 'member_of' => $groups);
279     }
280   }
281
282   function isPasswordExternal() {
283     return true;
284   }
285 }