Location: PHPKode > projects > Timesheet Next Gen > timesheet.ng-1.5.0/class.AuthenticationManager.php
<?php

error_reporting(E_ALL);
ini_set('display_errors', true);

/**
 * Added 11 Dec 2008
 * Installation script. As this file is called by every other file,
 * it makes sense to check to see if this app has been installed here.
 */	
if(
	!file_exists(dirname(__FILE__).DIRECTORY_SEPARATOR.'database_credentials.inc') ||
	!file_exists(dirname(__FILE__).DIRECTORY_SEPARATOR.'table_names.inc') 
	) {
	// this app has not been installed yet, redirect to the installation pages
	header("Location: ./install/");
	exit;
} else if(file_exists(dirname(__FILE__).DIRECTORY_SEPARATOR.'install')) {
	// the install directory has not been deleted, redirect to "delete install dir" pages
	// NOTE: this may also mean that the user has upgraded. The "delete install dir"
	// pages also check the version number to see if the user is running the latest
	// version, if not takes user to the "upgrade" pages
	header("Location: ./install/");
	exit;	
}
	
require("table_names.inc");
require("common.inc");
require("timezone.inc");
require("enum.php");

//define constants for error code
enum(
	"AUTH_NONE", //no attempt has been made to authenticate yet
	"AUTH_SUCCESS", //authentication succeeded
	"AUTH_FAILED_INCORRECT_PASSWORD", //incorrect password
	"AUTH_FAILED_NO_USERNAME_PASSED", //error no username was passed
	"AUTH_FAILED_EMPTY_PASSWORD", //error empty password not allowed
	"AUTH_LOGOUT", //user logged out
	"AUTH_FAILED_LDAP_LOGIN", //failed login via LDAP, check ldapErrorCode
	"AUTH_FAILED_NO_LDAP_MODULE", //no ldap module detected
	"AUTH_FAILED_INACTIVE" //error account is marked as inactive
);

//define constants for ldap error code
enum (
	"LDAP_AUTH_NONE", //no LDAP authentication has been attempted
	"LDAP_CONNECTION_FAILED", //connection failed
	"LDAP_MULTIPLE_ENTRIES_RETURNED", //multiple entries were returned
	"LDAP_SERVER_ERROR", //server error, check server error code
	"LDAP_USER_NOT_FOUND" //user not found
);

//define clearance levels
define("CLEARANCE_ADMINISTRATOR", 10);
define("CLEARANCE_MANAGER", 5);
define("CLEARANCE_BASIC", 0);

/**
*	Manages and provides authentication services
*/
class AuthenticationManager {

	/**
	*	The error code
	*/
	var $errorCode = AUTH_NONE;

	/**
	* The error text
	*/
	var $errorText = "Authentication has not yet been attempted";

	/**
	*	The error code to check if errorCode=AUTH_FAILED_LDAP_LOGIN
	*/
	var $ldapErrorCode;

	/**
	*	The error text which matches the ldapErrorCode
	*/
	var $ldapErrorText;

	/**
	* The error code returned from the LDAP server
	*/
	var $ldapServerErrorCode;

	/**
	* The error description returned from the LDAP server
	*/
	var $ldapServerErrorText;

	/* authentication function: this is called by
	*   each page to ensure that there is an authenticated user
	*/
	function login($username, $password) {
		require("table_names.inc");
		require("database_credentials.inc");

		//start/continue the session
		session_start();

		//set initial error codes
		$this->errorCode = AUTH_NONE;
		$this->errorText = "No attempt has been made to authenticate yet";
		$this->ldapErrorCode = LDAP_AUTH_NONE;
		$this->ldapErrorText = "No attempt has been made to authenticate via LDAP yet";
		$this->ldapServerErrorCode = 0;
		$this->ldapServerErrorText = "[]";

		//a username must be passed
		if (empty($username)) {
			$this->logout();
			$this->errorCode = AUTH_FAILED_NO_USERNAME_PASSED;
			$this->errorText = "You must enter a username";
			return false;
		}

		//a password must be passed
		if (empty($password)) {
			$this->logout();
			$this->errorCode = AUTH_FAILED_EMPTY_PASSWORD;
			$this->errorText = "You must enter a password";
			return false;
		}

		//connect to the database
		$dbh = dbConnect();

		//check whether we are using ldap
		if ($this->usingLDAP()) {
			//check their credentials with LDAP
			if ( !$this->ldapAuth($username, $password) ) {
				if ($this->usingFallback()) {
					if(!$this->dbAuth($username, $password)){
						return false;
					}
				} else {
					return false;
				}
			}
		} else {
			if(!$this->dbAuth($username, $password)){
				return false;
			}
		}

		//get the access level
		list($qh,$num) = dbQuery("SELECT level ".
									"FROM $USER_TABLE WHERE username='$username'");
		$data = dbResult($qh);

		//Fix session ID vulnerability: if someone installs this system locally, and creates the same username as an 
		//administrator/manager/ etc, making the session_id a hash from the username, password, and the access level 
		//should ensure the real production system won't allow them in without logging in, unless all that data is 
		//exactly the same.

		session_unset();
		session_destroy();

		$session_id=md5($username.$password.uniqid());
		//if we want to use a non-random string (for testing)
		//$session_id=md5($username.$password.$data["level"]);
		session_id($session_id);
		session_start();

		//set session variables
		$_SESSION["loggedInUser"] = $username;
		$_SESSION["accessLevel"] = $data["level"];
		$_SESSION["contextUser"] = $username;

		dbquery("UPDATE $USER_TABLE SET session='$session_id' WHERE username='$username'");

		$this->errorCode = AUTH_SUCCESS;
		$this->errorText = "Authentication succeeded";

		return true;
	}

	/* 
	* Login using ldap database	
	*/
	function ldapAuth($username, $password){
		// check that the module is availble
		$ldapMaxLinks = ini_get( "ldap.max_links" );
		if ( empty( $ldapMaxLinks ) ){
			$this->errorCode = AUTH_FAILED_NO_LDAP_MODULE;
			$this->errorText = "Could not access LDAP module - is it installed?";
			return false;
		}
		// check their credentials with LDAP
		if ( !$this->ldapLogin( $username, $password ) ){
			if($this->errorCode == AUTH_NONE ) { //error code hasn't been set to some other error
				$this->errorCode = AUTH_FAILED_LDAP_LOGIN;
				$this->errorText = "Authentication via LDAP failed";
			}
			return false;
		}
		return true;
	}
	
	/* 
	* Login using local database 
	*/
	function dbAuth($username, $password){
		require( "table_names.inc" );
		require( "database_credentials.inc" );
		// query the user table for authentication details
		list( $qh, $num ) = dbQuery( "SELECT password AS passwd1, $DATABASE_PASSWORD_FUNCTION('$password') AS passwd2, status " . "FROM $USER_TABLE WHERE username='$username'" );
		$data = dbResult( $qh );
		// is the password correct?
		if ( $num == 0 || $data["passwd1"] != $data["passwd2"] ){
			$this->errorCode =  AUTH_FAILED_INCORRECT_PASSWORD;
			if((isset($this->errorText))){
				$this->errorText = $this->errorText . " or Incorrect username or password";
			} else {
				$this->errorText = "Incorrect username or password";
			}
			return false;
		}

		if ( $data['status'] == 'INACTIVE') {
			$this->errorCode = AUTH_FAILED_INACTIVE; 
			$this->errorText = "That account is marked INACTIVE";
			return false;
		}

		return true;
	}

	/**
	* Logs out the currenlty logged in user
	*/
	function logout() {
		require("table_names.inc");
		require("database_credentials.inc");

		//start/continue the session
		session_start();

		if($this->isLoggedIn()) {
			$username=$_SESSION['loggedInUser'];
			dbquery("UPDATE $USER_TABLE SET session='logged out' WHERE username='$username'");
		}

		//unset all the variables
		session_unset();

		//destroy the session
		session_destroy();

		$this->errorCode = AUTH_LOGOUT;
		$this->errorText = "The user was logged out";
		return;
	}

	/**
	*	returns true if the user is logged in
	*/
	function isLoggedIn() {
		require( "table_names.inc" );

		//start/continue the session
		@session_start();

		$session_id=session_id();
		if(empty($_SESSION['loggedInUser'])) return false;
		$username=$_SESSION['loggedInUser'];

		list( $qh, $num ) = dbQuery( "SELECT session FROM $USER_TABLE WHERE username='$username'" );
		if($num != 1) return false;
		$data = dbResult( $qh );
		if($data['session'] != $session_id) return false;

		return !empty($_SESSION['accessLevel']) && !empty($_SESSION['loggedInUser']) && !empty($_SESSION['contextUser']);
	}

	/**
	* returns true if the user has clearance to the specified level
	*/
	function hasClearance($accessLevel) {
		//start/continue the session
		@session_start();

		return (isset($_SESSION['accessLevel']) && $_SESSION['accessLevel'] >= $accessLevel);
	}

	/**
	* returns true if the user has access to the specified page
	*/
	function hasAccess($page) {

		$acl = get_acl_level($page);
		switch ($acl) {
		case 'None':
			$level = 100; //This level is unobtainable
			break;
		case 'Basic':
			$level = CLEARANCE_BASIC;
			break;
		case 'Mgr':
			$level = CLEARANCE_MANAGER;
			break;
		default:
			$level = CLEARANCE_ADMINISTRATOR;
			break;
		}
		return ($this->hasClearance($level));
	}

	/* This function returns true if the system is configured to use
	*	LDAP for authentication
	*/
	function usingLDAP() {
		require("table_names.inc");
		list($qh, $num) = dbQuery("SELECT useLDAP FROM $CONFIG_TABLE WHERE config_set_id='1'");
		$data = dbResult($qh);
		return $data['useLDAP'] == 1;
	}

	/* 
	* This function returns true if the system is configured to fallback to local authentication should LDAP authentication fail
	* DB Field should perhaps be renamed to LOCALFallback
	*/
	function usingFallback(){
		require( "table_names.inc" );
		list( $qh, $num ) = dbQuery( "SELECT BIN(LDAPFallback) FROM $CONFIG_TABLE WHERE config_set_id='1'" );
		$data = dbResult( $qh );
		return $data['BIN(LDAPFallback)'] == 1;
	}

	function ldapLogin($username, $password) {
		require("table_names.inc");
		require("database_credentials.inc");

		//require("debuglog.php");
		//$debug = new logfile();

		//get the connection settings from the database
		list($qh, $num) = dbQuery("SELECT LDAPScheme, LDAPHost, LDAPPort, LDAPBaseDN, " .
						"LDAPUsernameAttribute, LDAPSearchScope, LDAPFilter, LDAPProtocolVersion, " .
						"LDAPBindByUser, LDAPBindUsername, LDAPBindPassword, LDAPReferrals " .
						"FROM $CONFIG_TABLE WHERE config_set_id='1'");
		$data = dbResult($qh);

		//build up connection string
		$connectionString = $data['LDAPScheme'] . "://" . $data['LDAPHost'] . ":" . $data['LDAPPort'];

		//$debug->write("connectionString = $connectionString\n");

		//connect to server
		//echo "connecting to server: $connectionString <p>";
		if (!($connection = @ldap_connect($connectionString))) {
			$this->ldapErrorCode = LDAP_CONNECTION_FAILED;
			$this->ldapErrorText = "Failed to connect to ldap server at $connectionString";
			return false;
		}

		//attempt to set the protocol version to use
		@ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, $data["LDAPProtocolVersion"]);

		// bind to server by user
		if ($data['LDAPBindByUser'] == 1) {
			if (empty($data["LDAPBindUsername"])) {
				//bind using user supplied info, if there is no BindUsername in the config
				$credentials=$data['LDAPUsernameAttribute'] . "=" . $username . "," . $data['LDAPBaseDN'];

				if (!($bind = @ldap_bind($connection, $credentials, $password))) {
					$this->ldapErrorCode = LDAP_SERVER_ERROR;
					$this->ldapServerErrorCode = ldap_errno($connection);
					$this->ldapServerErrorText = "LDAP: " . ldap_error($connection);
					return false;
				}
			} else {
				//bind to server with config provided username and password
				if (!($bind = @ldap_bind($connection, $data["LDAPBindUsername"], $data["LDAPBindPassword"]))) {
					$this->ldapErrorCode = LDAP_SERVER_ERROR;
					$this->ldapServerErrorCode = ldap_errno($connection);
					$this->ldapServerErrorText = "LDAP: " . ldap_error($connection);
					return false;
				}
			}
		} else {
			//bind to server (anonymously)
			if (!($bind = @ldap_bind($connection))) {
				$this->ldapErrorCode = LDAP_SERVER_ERROR;
				$this->ldapServerErrorCode = ldap_errno($connection);
				$this->ldapServerErrorText = "LDAP: " . ldap_error($connection);
				return false;
			}
		}

		//attempt to set the protocol version to use
		@ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, $data["LDAPProtocolVersion"]);

		//build up the filter by adding the username filter
		$filter = $data['LDAPUsernameAttribute'] . "=" . $username;
		if ($data['LDAPFilter'] != "") {
			//does it start with a '(' and end with a ')' ?
			$userFilter = $data["LDAPFilter"];
			$length = strlen($userFilter);
			if ($userFilter{0} == "(" && $userFilter{$length-1} == ")")
				$userFilter = substr($userFilter, 1, $length-2);

			$filter = "(&(" . $userFilter . ")(" . $filter . "))";
		}

		// Always avoid referals if flag is not set (they are enabled by default in php ldap module)
		if ( $data["LDAPReferrals"] != 1) {
			@ldap_set_option( $connection, LDAP_OPT_REFERRALS, 0 );
		}

		if ($data["LDAPSearchScope"] == "base") {
			//search the directory returning records in the base dn
			//echo "<p>searching base dn: $data[LDAPBaseDN]        with filter: $filter <p>";
			//$debug->write("searching base ".$data['LDAPBaseDN']." for $filter\n");
			if (!($search = @ldap_read($connection, $data['LDAPBaseDN'], $filter))) {
				$this->ldapErrorCode = LDAP_SERVER_ERROR;
				$this->ldapServerErrorCode = ldap_errno($connection);
				$this->ldapServerErrorText = "LDAP: " . ldap_error($connection);
				return false;
			}
		} else if ($data["LDAPSearchScope"] == "one") {
			//search the directory returning records in the base dn
			//echo "<p>searching base dn: $data[LDAPBaseDN]        with filter: $filter <p>";
			//$debug->write("searching one ".$data['LDAPBaseDN']." for $filter\n");
			if (!($search = @ldap_list($connection, $data['LDAPBaseDN'], $filter))) {
				$this->ldapErrorCode = LDAP_SERVER_ERROR;
				$this->ldapServerErrorCode = ldap_errno($connection);
				$this->ldapServerErrorText = "LDAP: " . ldap_error($connection);
				return false;
			}
		} else { //full subtree search
			//search the directory returning records in the base dn
			//echo "<p>searching base dn: $data[LDAPBaseDN]        with filter: $filter <p>";
			//$debug->write("searching sub ".$data['LDAPBaseDN']." for $filter\n");
			if (!($search = @ldap_search($connection, $data['LDAPBaseDN'], $filter))) {
				$this->ldapErrorCode = LDAP_SERVER_ERROR;
				$this->ldapServerErrorCode = ldap_errno($connection);
				$this->ldapServerErrorText = "LDAP: " . ldap_error($connection);
				return false;
			}
		}

		//get the results
		$numberOfEntries = ldap_count_entries($connection,$search);

		if ($numberOfEntries == 0) {
			$this->ldapErrorCode = LDAP_USER_NOT_FOUND;
			$this->ldapErrorText = "The user was not found in the LDAP database";
			return false;
		}

		//there must be 1 and only 1 result
		if ($numberOfEntries > 1) {
			$this->ldapErrorCode = LDAP_MULTIPLE_ENTRIES_RETURNED;
			$this->ldapErrorText = "Multiple entries were returned for that username";
			return false;
		}

		//get the entry
		$entry = ldap_first_entry($connection, $search);

		//get the entries dn
		$entryDN = ldap_get_dn($connection, $entry);
		//print "<p>The entry was found and its DN is '" . $entryDN . "'</p>";

		//now try a bind with this dn and the password
		if (!($userBind = @ldap_bind($connection, $entryDN, $password))) {
			$this->ldapErrorCode = LDAP_SERVER_ERROR;
			$this->ldapServerErrorCode = ldap_errno($connection);
			$this->ldapServerErrorText = "LDAP: " . ldap_error($connection);
			return false;
		}

		//get the attributes for this entry
		$attributes = ldap_get_attributes($connection, $entry);

		//get some info from the first entry to update into the db
		$lastName = $attributes['sn'][0];
		if (!isset($attributes['givenName'])) {
			if (isset($attributes['cn'])) {
				$spacePos = strpos($attributes['cn'][0], " ");
				if (!($spacePos === false))
					$firstName = substr($attributes['cn'][0], 0, $spacePos);
				else
					$firstName = $attributes['cn'][0];
			} else
				$firstName = $lastName;
		} else
			$firstName = $attributes['givenName'][0];

		$emailAddress = isset($attributes['mail']) ? $attributes['mail'][0]: "";

		//does the user exist in the db?
		if (!$this->userExists($username)) {
			//create the user
			if ($this->usingFallback()) //if we're using Fallback, then we want to put the password in the database
				$pwdstr = "$DATABASE_PASSWORD_FUNCTION('$password')";
			else 
				$pwdstr = "";
			dbquery("INSERT INTO $USER_TABLE (username, level, password, first_name, last_name, " .
						"email_address, time_stamp, status) " .
						"VALUES ('$username',1,$pwdstr,'$firstName',".
						"'$lastName','$emailAddress',0,'ACTIVE')");
			dbquery("INSERT INTO $ASSIGNMENTS_TABLE VALUES (1,'$username', 1)"); // add default project.
			dbquery("INSERT INTO $TASK_ASSIGNMENTS_TABLE VALUES (1,'$username', 1)"); // add default task
		} else {
			//get the existing user details
			list($qh, $num) = dbQuery("SELECT first_name, last_name, email_address, status " .
							"FROM $USER_TABLE WHERE username='$username'");
			$existingUserDetails = dbResult($qh);

			if($existingUserDetails['status'] == 'INACTIVE') {
				$this->errorCode = AUTH_FAILED_INACTIVE; 
				$this->errorText = "That account is marked INACTIVE";
				return false;
			}
			
			//use existing ones if needs be
			if ($firstName == "")
				$firstName = $existingUserDetails['first_name'];
			if ($lastName == "")
				$lastName = $existingUserDetails['last_name'];
			if ($emailAddress == "")
				$emailAddress = $existingUserDetails['email_address'];

			//update the users details
			if ($this->usingFallback()) //if we're using Fallback, then we want to store the current password in the database
				$pwdstr = "$DATABASE_PASSWORD_FUNCTION('$password')";
			else 
				$pwdstr = "''";
			dbquery("UPDATE $USER_TABLE SET first_name='$firstName', last_name='$lastName', ".
								"email_address='$emailAddress', password=$pwdstr ".
								"WHERE username='$username'");
		}

		//login succeeded, returning true
		return true;
	}

	/**
	* returns true if there is a record under that username in the database
	*/
	function userExists($username) {
		require("table_names.inc");

		//check whether the user exists
		list($qh,$num) = dbQuery("SELECT username FROM $USER_TABLE WHERE username='$username'");

		//if there is a match
		return ($data = dbResult($qh));
	}

	/**
	* returns a string with the reason the login failed
	*/
	function getErrorMessage() {
		if ($this->errorCode != AUTH_FAILED_LDAP_LOGIN)
			return $this->errorText;

		if ($this->ldapErrorCode != LDAP_SERVER_ERROR)
			return $this->ldapErrorText;

		return $this->ldapServerErrorText;
	}
}

//create the instance so its availiable by just including this file
$authenticationManager = new AuthenticationManager;

// vim:ai:ts=4:sw=4
?>
Return current item: Timesheet Next Gen