2 if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3 /*********************************************************************************
4 * SugarCRM Community Edition is a customer relationship management program developed by
5 * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
7 * This program is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU Affero General Public License version 3 as published by the
9 * Free Software Foundation with the addition of the following permission added
10 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
12 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
19 * You should have received a copy of the GNU Affero General Public License along with
20 * this program; if not, see http://www.gnu.org/licenses or write to the Free
21 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
25 * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
27 * The interactive user interfaces in modified source and object code versions
28 * of this program must display Appropriate Legal Notices, as required under
29 * Section 5 of the GNU Affero General Public License version 3.
31 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32 * these Appropriate Legal Notices must retain the display of the "Powered by
33 * SugarCRM" logo. If the display of the logo is not reasonably feasible for
34 * technical reasons, the Appropriate Legal Notices must display the words
35 * "Powered by SugarCRM".
36 ********************************************************************************/
42 * This file is where the user authentication occurs. No redirection should happen in this file.
45 require_once('modules/Users/authentication/LDAPAuthenticate/LDAPConfigs/default.php');
46 require_once('modules/Users/authentication/SugarAuthenticate/SugarAuthenticateUser.php');
48 define('DEFAULT_PORT', 389);
49 class LDAPAuthenticateUser extends SugarAuthenticateUser{
52 * Does the actual authentication of the user and returns an id that will be used
53 * to load the current user (loadUserOnSession)
56 * @param STRING $password
57 * @return STRING id - used for loading the user
59 * Contributions by Erik Mitchell erikm@logicpd.com
61 function authenticateUser($name, $password) {
63 $server = $GLOBALS['ldap_config']->settings['ldap_hostname'];
64 $port = $GLOBALS['ldap_config']->settings['ldap_port'];
67 $GLOBALS['log']->debug("ldapauth: Connecting to LDAP server: $server");
68 $ldapconn = ldap_connect($server, $port);
69 $error = ldap_errno($ldapconn);
70 if($this->loginError($error)){
73 @ldap_set_option($ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3);
74 @ldap_set_option($ldapconn, LDAP_OPT_REFERRALS, 0); // required for AD
75 // If constant is defined, set the timeout (PHP >= 5.3)
76 if (defined('LDAP_OPT_NETWORK_TIMEOUT'))
78 // Network timeout, lower than PHP and DB timeouts
79 @ldap_set_option($ldapconn, LDAP_OPT_NETWORK_TIMEOUT, 60);
82 $bind_user = $this->ldap_rdn_lookup($name, $password);
83 $GLOBALS['log']->debug("ldapauth.ldap_authenticate_user: ldap_rdn_lookup returned bind_user=" . $bind_user);
85 $GLOBALS['log']->fatal("SECURITY: ldapauth: failed LDAP bind (login) by " .
86 $name . ", could not construct bind_user");
90 // MRF - Bug #18578 - punctuation was being passed as HTML entities, i.e. &
91 $bind_password = html_entity_decode($password,ENT_QUOTES);
93 $GLOBALS['log']->info("ldapauth: Binding user " . $bind_user);
94 $bind = ldap_bind($ldapconn, $bind_user, $bind_password);
95 $error = ldap_errno($ldapconn);
96 if ($this->loginError($error)) {
97 $full_user = $GLOBALS['ldap_config']->settings['ldap_bind_attr'] . "=" . $bind_user
98 . "," . $GLOBALS['ldap_config']->settings['ldap_base_dn'];
100 $GLOBALS['log']->info("ldapauth: Binding user " . $full_user);
101 $bind = ldap_bind($ldapconn, $full_user, $bind_password);
102 $error = ldap_errno($ldapconn);
103 if ($this->loginError($error)) {
108 $GLOBALS['log']->info("ldapauth: Bind attempt complete.");
111 // Authentication succeeded, get info from LDAP directory
112 $attrs = array_keys($GLOBALS['ldapConfig']['users']['fields']);
113 $base_dn = $GLOBALS['ldap_config']->settings['ldap_base_dn'];
114 $name_filter = $this->getUserNameFilter($name);
116 //add the group user attribute that we will compare to the group attribute for membership validation if group membership is turned on
117 if (!empty($GLOBALS['ldap_config']->settings['ldap_group'])
118 && !empty($GLOBALS['ldap_config']->settings['ldap_group_user_attr'])
119 && !empty($GLOBALS['ldap_config']->settings['ldap_group_attr'])) {
121 if (!in_array($attrs, $GLOBALS['ldap_config']->settings['ldap_group_user_attr'])) {
122 $attrs[] = $GLOBALS['ldap_config']->settings['ldap_group_user_attr'];
126 $GLOBALS['log']->debug(
127 "ldapauth: Fetching user info from Directory using base dn: "
128 . $base_dn . ", name_filter: " . $name_filter . ", attrs: " . var_export($attrs, true)
131 $result = @ldap_search($ldapconn, $base_dn, $name_filter, $attrs);
132 $error = ldap_errno($ldapconn);
133 if ($this->loginError($error)) {
136 $GLOBALS['log']->debug("ldapauth: ldap_search complete.");
138 $info = @ldap_get_entries($ldapconn, $result);
139 $error = ldap_errno($ldapconn);
140 if ($this->loginError($error)) {
146 $GLOBALS['log']->debug("ldapauth: User info from Directory fetched.");
148 // some of these don't seem to work
149 $this->ldapUserInfo = array();
150 foreach($GLOBALS['ldapConfig']['users']['fields'] as $key=>$value){
152 $key = strtolower($key);
153 if(isset($info[0]) && isset($info[0][$key]) && isset($info[0][$key][0])){
154 $this->ldapUserInfo[$value] = $info[0][$key][0];
158 //we should check that a user is a member of a specific group
159 if(!empty($GLOBALS['ldap_config']->settings['ldap_group'])){
160 $GLOBALS['log']->debug("LDAPAuth: scanning group for user membership");
161 $group_user_attr = $GLOBALS['ldap_config']->settings['ldap_group_user_attr'];
162 $group_attr = $GLOBALS['ldap_config']->settings['ldap_group_attr'];
163 if(!isset($info[0][$group_user_attr])){
164 $GLOBALS['log']->fatal("ldapauth: $group_user_attr not found for user $name cannot authenticate against an LDAP group");
165 ldap_close($ldapconn);
168 $user_uid = $info[0][$group_user_attr];
169 if (is_array($user_uid)){
170 $user_uid = $user_uid[0];
172 // If user_uid contains special characters (for LDAP) we need to escape them !
173 $user_uid = str_replace(array("(", ")"), array("\(", "\)"), $user_uid);
177 // build search query and determine if we are searching for a bare id or the full dn path
178 $group_name = $GLOBALS['ldap_config']->settings['ldap_group_name'] . ","
179 . $GLOBALS['ldap_config']->settings['ldap_group_dn'];
180 $GLOBALS['log']->debug("ldapauth: Searching for group name: " . $group_name);
182 if (!empty($GLOBALS['ldap_config']->settings['ldap_group_attr_req_dn'])
183 && $GLOBALS['ldap_config']->settings['ldap_group_attr_req_dn'] == 1) {
185 $GLOBALS['log']->debug("ldapauth: Checking for group membership using full user dn");
186 $user_search = "($group_attr=" . $group_user_attr . "=" . $user_uid . "," . $base_dn . ")";
188 $user_search = "($group_attr=" . $user_uid . ")";
190 $GLOBALS['log']->debug("ldapauth: Searching for user: " . $user_search);
192 //user is not a member of the group if the count is zero get the logs and return no id so it fails login
193 if (!isset($user_uid)
194 || ldap_count_entries($ldapconn, ldap_search($ldapconn, $group_name, $user_search)) == 0) {
196 $GLOBALS['log']->fatal("ldapauth: User ($name) is not a member of the LDAP group");
197 $user_id = var_export($user_uid, true);
198 $GLOBALS['log']->debug(
199 "ldapauth: Group DN:{$GLOBALS['ldap_config']->settings['ldap_group_dn']}"
200 . " Group Name: " . $GLOBALS['ldap_config']->settings['ldap_group_name']
201 . " Group Attribute: $group_attr User Attribute: $group_user_attr :(" . $user_uid . ")"
204 ldap_close($ldapconn);
211 ldap_close($ldapconn);
212 $dbresult = $GLOBALS['db']->query("SELECT id, status FROM users WHERE user_name='" . $name . "' AND deleted = 0");
214 //user already exists use this one
215 if($row = $GLOBALS['db']->fetchByAssoc($dbresult)){
216 if($row['status'] != 'Inactive')
222 //create a new user and return the user
223 if($GLOBALS['ldap_config']->settings['ldap_auto_create_users']){
224 return $this->createUser($name);
230 $GLOBALS['log']->fatal("SECURITY: failed LDAP bind (login) by $this->user_name using bind_user=$bind_user");
231 $GLOBALS['log']->fatal("ldapauth: failed LDAP bind (login) by $this->user_name using bind_user=$bind_user");
232 ldap_close($ldapconn);
238 * takes in a name and creates the appropriate search filter for that user name including any additional filters specified in the system settings page
242 function getUserNameFilter($name){
243 $name_filter = "(" . $GLOBALS['ldap_config']->settings['ldap_login_attr']. "=" . $name . ")";
244 //add the additional user filter if it is specified
245 if(!empty($GLOBALS['ldap_config']->settings['ldap_login_filter'])){
246 $add_filter = $GLOBALS['ldap_config']->settings['ldap_login_filter'];
247 if(substr($add_filter, 0, 1) !== "("){
248 $add_filter = "(" . $add_filter . ")";
250 $name_filter = "(&" . $name_filter . $add_filter . ")";
256 * Creates a user with the given User Name and returns the id of that new user
257 * populates the user with what was set in ldapUserInfo
259 * @param STRING $name
262 function createUser($name){
265 $user->user_name = $name;
266 foreach($this->ldapUserInfo as $key=>$value){
267 $user->$key = $value;
269 $user->employee_status = 'Active';
270 $user->status = 'Active';
272 $user->external_auth_only = 1;
278 * this is called when a user logs in
280 * @param STRING $name
281 * @param STRING $password
284 function loadUserOnLogin($name, $password) {
288 // Check if the LDAP extensions are loaded
289 if(!function_exists('ldap_connect')) {
290 $error = $mod_strings['LBL_LDAP_EXTENSION_ERROR'];
291 $GLOBALS['log']->fatal($error);
292 $_SESSION['login_error'] = $error;
297 $GLOBALS['ldap_config'] = new Administration();
298 $GLOBALS['ldap_config']->retrieveSettings('ldap');
299 $GLOBALS['log']->debug("Starting user load for ". $name);
300 if(empty($name) || empty($password)) return false;
301 checkAuthUserStatus();
303 $user_id = $this->authenticateUser($name, $password);
304 if(empty($user_id)) {
305 //check if the user can login as a normal sugar user
306 $GLOBALS['log']->fatal('SECURITY: User authentication for '.$name.' failed');
309 $this->loadUserOnSession($user_id);
315 * Called with the error number of the last call if the error number is 0
316 * there was no error otherwise it converts the error to a string and logs it as fatal
321 function loginError($error){
322 if(empty($error)) return false;
323 $errorstr = ldap_err2str($error);
325 $_SESSION['login_error'] = $errorstr;
328 $_SESSION['login_error'] = translate('ERR_INVALID_PASSWORD', 'Users');
332 $GLOBALS['log']->fatal('[LDAP ERROR]['. $error . ']'.$errorstr);
337 * @return string appropriate value for username when binding to directory server.
338 * @param string $user_name the value provided in login form
339 * @desc Take the login username and return either said username for AD or lookup
340 * distinguished name using anonymous credentials for OpenLDAP.
341 * Contributions by Erik Mitchell erikm@logicpd.com
343 function ldap_rdn_lookup($user_name, $password) {
345 // MFH BUG# 14547 - Added htmlspecialchars_decode()
346 $server = $GLOBALS['ldap_config']->settings['ldap_hostname'];
347 $base_dn = htmlspecialchars_decode($GLOBALS['ldap_config']->settings['ldap_base_dn']);
348 if(!empty($GLOBALS['ldap_config']->settings['ldap_authentication'])){
349 $admin_user = htmlspecialchars_decode($GLOBALS['ldap_config']->settings['ldap_admin_user']);
350 $admin_password = htmlspecialchars_decode($GLOBALS['ldap_config']->settings['ldap_admin_password']);
353 $admin_password = '';
355 $user_attr = $GLOBALS['ldap_config']->settings['ldap_login_attr'];
356 $bind_attr = $GLOBALS['ldap_config']->settings['ldap_bind_attr'];
357 $port = $GLOBALS['ldap_config']->settings['ldap_port'];
359 $port = DEFAULT_PORT;
360 $ldapconn = ldap_connect($server, $port);
361 $error = ldap_errno($ldapconn);
362 if($this->loginError($error)){
365 ldap_set_option($ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3);
366 ldap_set_option($ldapconn, LDAP_OPT_REFERRALS, 0); // required for AD
367 //if we are going to connect anonymously lets at least try to connect with the user connecting
368 if(empty($admin_user)){
369 $bind = @ldap_bind($ldapconn, $user_name, $password);
370 $error = ldap_errno($ldapconn);
373 $bind = @ldap_bind($ldapconn, $admin_user, $admin_password);
374 $error = ldap_errno($ldapconn);
377 if($this->loginError($error)){
381 $GLOBALS['log']->warn("ldapauth.ldap_rdn_lookup: Could not bind with admin user, trying to bind anonymously");
382 $bind = @ldap_bind($ldapconn);
383 $error = ldap_errno($ldapconn);
385 if($this->loginError($error)){
389 $GLOBALS['log']->warn("ldapauth.ldap_rdn_lookup: Could not bind anonymously, returning username");
394 // If we get here we were able to bind somehow
395 $search_filter = $this->getUserNameFilter($user_name);
397 $GLOBALS['log']->info("ldapauth.ldap_rdn_lookup: Bind succeeded, searching for $user_attr=$user_name");
398 $GLOBALS['log']->debug("ldapauth.ldap_rdn_lookup: base_dn:$base_dn , search_filter:$search_filter");
400 $result = @ldap_search($ldapconn, $base_dn , $search_filter, array("dn", $bind_attr));
401 $error = ldap_errno($ldapconn);
402 if($this->loginError($error)){
405 $info = ldap_get_entries($ldapconn, $result);
406 if($info['count'] == 0){
411 ldap_unbind($ldapconn);
413 $GLOBALS['log']->info("ldapauth.ldap_rdn_lookup: Search result:\nldapauth.ldap_rdn_lookup: " . count($info));
415 if ($bind_attr == "dn") {
416 $found_bind_user = $info[0]['dn'];
418 $found_bind_user = $info[0][strtolower($bind_attr)][0];
421 $GLOBALS['log']->info("ldapauth.ldap_rdn_lookup: found_bind_user=" . $found_bind_user);
423 if (!empty($found_bind_user)) {
424 return $found_bind_user;
425 } elseif ($user_attr == $bind_attr) {