2020-11-30 17:11:44 -08:00
< ? php
2021-06-10 13:15:52 -07:00
2020-11-30 17:11:44 -08:00
namespace App\Models ;
use App\Models\Setting ;
2021-06-10 13:15:52 -07:00
use App\Models\User ;
2020-11-30 17:11:44 -08:00
use Exception ;
2021-06-10 13:15:52 -07:00
use Illuminate\Database\Eloquent\Model ;
2020-11-30 17:11:44 -08:00
use Input ;
use Log ;
class Ldap extends Model
{
/**
* Makes a connection to LDAP using the settings in Admin > Settings .
*
* @ author [ A . Gianotto ] [ < snipe @ snipe . net > ]
* @ since [ v3 . 0 ]
* @ return connection
*/
public static function connectToLdap ()
{
2021-06-10 13:15:52 -07:00
$ldap_host = Setting :: getSettings () -> ldap_server ;
2020-11-30 17:11:44 -08:00
$ldap_version = Setting :: getSettings () -> ldap_version ;
$ldap_server_cert_ignore = Setting :: getSettings () -> ldap_server_cert_ignore ;
$ldap_use_tls = Setting :: getSettings () -> ldap_tls ;
// If we are ignoring the SSL cert we need to setup the environment variable
// before we create the connection
2021-06-10 13:15:52 -07:00
if ( $ldap_server_cert_ignore == '1' ) {
2020-11-30 17:11:44 -08:00
putenv ( 'LDAPTLS_REQCERT=never' );
}
// If the user specifies where CA Certs are, make sure to use them
2021-06-10 13:15:52 -07:00
if ( env ( 'LDAPTLS_CACERT' )) {
putenv ( 'LDAPTLS_CACERT=' . env ( 'LDAPTLS_CACERT' ));
2020-11-30 17:11:44 -08:00
}
$connection = @ ldap_connect ( $ldap_host );
2021-06-10 13:15:52 -07:00
if ( ! $connection ) {
2020-11-30 17:11:44 -08:00
throw new Exception ( 'Could not connect to LDAP server at ' . $ldap_host . '. Please check your LDAP server name and port number in your settings.' );
}
// Needed for AD
ldap_set_option ( $connection , LDAP_OPT_REFERRALS , 0 );
ldap_set_option ( $connection , LDAP_OPT_PROTOCOL_VERSION , $ldap_version );
ldap_set_option ( $connection , LDAP_OPT_NETWORK_TIMEOUT , 20 );
2021-08-17 14:43:36 -07:00
if ( Setting :: getSettings () -> ldap_client_tls_cert && Setting :: getSettings () -> ldap_client_tls_key ) {
ldap_set_option ( $connection , LDAP_OPT_X_TLS_CERTFILE , Setting :: get_client_side_cert_path ());
ldap_set_option ( $connection , LDAP_OPT_X_TLS_KEYFILE , Setting :: get_client_side_key_path ());
}
2020-11-30 17:11:44 -08:00
if ( $ldap_use_tls == '1' ) {
ldap_start_tls ( $connection );
}
2021-08-17 14:43:36 -07:00
2020-11-30 17:11:44 -08:00
return $connection ;
}
/**
* Binds / authenticates the user to LDAP , and returns their attributes .
*
* @ author [ A . Gianotto ] [ < snipe @ snipe . net > ]
* @ since [ v3 . 0 ]
* @ param $username
* @ param $password
* @ param bool | false $user
* @ return bool true if the username and / or password provided are valid
* false if the username and / or password provided are invalid
* array of ldap_attributes if $user is true
*/
2021-06-10 13:15:52 -07:00
public static function findAndBindUserLdap ( $username , $password )
2020-11-30 17:11:44 -08:00
{
$settings = Setting :: getSettings ();
2021-06-10 13:15:52 -07:00
$connection = self :: connectToLdap ();
$ldap_username_field = $settings -> ldap_username_field ;
$baseDn = $settings -> ldap_basedn ;
$userDn = $ldap_username_field . '=' . $username . ',' . $settings -> ldap_basedn ;
2020-11-30 17:11:44 -08:00
2021-06-10 13:15:52 -07:00
if ( $settings -> is_ad == '1' ) {
2020-11-30 17:11:44 -08:00
// Check if they are using the userprincipalname for the username field.
// If they are, we can skip building the UPN to authenticate against AD
2021-06-10 13:15:52 -07:00
if ( $ldap_username_field == 'userprincipalname' ) {
2020-11-30 17:11:44 -08:00
$userDn = $username ;
} else {
// In case they haven't added an AD domain
2021-06-10 13:15:52 -07:00
$userDn = ( $settings -> ad_domain != '' ) ? $username . '@' . $settings -> ad_domain : $username . '@' . $settings -> email_domain ;
2020-11-30 17:11:44 -08:00
}
}
\Log :: debug ( 'Attempting to login using distinguished name:' . $userDn );
2021-06-10 13:15:52 -07:00
$filterQuery = $settings -> ldap_auth_filter_query . $username ;
2021-04-20 21:25:45 -07:00
$filter = Setting :: getSettings () -> ldap_filter ;
$filterQuery = " ( { $filter } ( { $filterQuery } )) " ;
\Log :: debug ( 'Filter query: ' . $filterQuery );
2020-11-30 17:11:44 -08:00
2021-06-10 13:15:52 -07:00
if ( ! $ldapbind = @ ldap_bind ( $connection , $userDn , $password )) {
if ( ! $ldapbind = self :: bindAdminToLdap ( $connection )) {
2020-11-30 17:11:44 -08:00
return false ;
}
}
2021-06-10 13:15:52 -07:00
if ( ! $results = ldap_search ( $connection , $baseDn , $filterQuery )) {
2020-11-30 17:11:44 -08:00
throw new Exception ( 'Could not search LDAP: ' );
}
2021-06-10 13:15:52 -07:00
if ( ! $entry = ldap_first_entry ( $connection , $results )) {
2020-11-30 17:11:44 -08:00
return false ;
}
2021-06-10 13:15:52 -07:00
if ( ! $user = ldap_get_attributes ( $connection , $entry )) {
2020-11-30 17:11:44 -08:00
return false ;
}
return array_change_key_case ( $user );
}
/**
* Binds / authenticates an admin to LDAP for LDAP searching / syncing .
* Here we also return a better error if the app key is donked .
*
* @ author [ A . Gianotto ] [ < snipe @ snipe . net > ]
* @ since [ v3 . 0 ]
* @ param bool | false $user
* @ return bool true if the username and / or password provided are valid
* false if the username and / or password provided are invalid
*/
2021-06-10 13:15:52 -07:00
public static function bindAdminToLdap ( $connection )
2020-11-30 17:11:44 -08:00
{
2021-06-10 13:15:52 -07:00
$ldap_username = Setting :: getSettings () -> ldap_uname ;
2020-11-30 17:11:44 -08:00
$ldap_username = Setting :: getSettings () -> ldap_uname ;
// Lets return some nicer messages for users who donked their app key, and disable LDAP
try {
2021-06-10 13:15:52 -07:00
$ldap_pass = \Crypt :: decrypt ( Setting :: getSettings () -> ldap_pword );
2020-11-30 17:11:44 -08:00
} catch ( Exception $e ) {
throw new Exception ( 'Your app key has changed! Could not decrypt LDAP password using your current app key, so LDAP authentication has been disabled. Login with a local account, update the LDAP password and re-enable it in Admin > Settings.' );
}
2021-06-10 13:15:52 -07:00
if ( ! $ldapbind = @ ldap_bind ( $connection , $ldap_username , $ldap_pass )) {
2020-11-30 17:11:44 -08:00
throw new Exception ( 'Could not bind to LDAP: ' . ldap_error ( $connection ));
}
}
/**
* Parse and map LDAP attributes based on settings
*
* @ author [ A . Gianotto ] [ < snipe @ snipe . net > ]
* @ since [ v3 . 0 ]
*
* @ param $ldapatttibutes
* @ return array | bool
*/
2021-06-10 13:15:52 -07:00
public static function parseAndMapLdapAttributes ( $ldapattributes )
2020-11-30 17:11:44 -08:00
{
//Get LDAP attribute config
$ldap_result_username = Setting :: getSettings () -> ldap_username_field ;
$ldap_result_emp_num = Setting :: getSettings () -> ldap_emp_num ;
$ldap_result_last_name = Setting :: getSettings () -> ldap_lname_field ;
$ldap_result_first_name = Setting :: getSettings () -> ldap_fname_field ;
$ldap_result_email = Setting :: getSettings () -> ldap_email ;
2021-04-05 19:26:04 -07:00
$ldap_result_phone = Setting :: getSettings () -> ldap_phone ;
$ldap_result_jobtitle = Setting :: getSettings () -> ldap_jobtitle ;
2021-06-10 13:15:52 -07:00
$ldap_result_country = Setting :: getSettings () -> ldap_country ;
2021-04-14 10:17:57 -07:00
$ldap_result_dept = Setting :: getSettings () -> ldap_dept ;
2020-11-30 17:11:44 -08:00
// Get LDAP user data
2021-06-10 13:15:52 -07:00
$item = [];
$item [ 'username' ] = isset ( $ldapattributes [ $ldap_result_username ][ 0 ]) ? $ldapattributes [ $ldap_result_username ][ 0 ] : '' ;
$item [ 'employee_number' ] = isset ( $ldapattributes [ $ldap_result_emp_num ][ 0 ]) ? $ldapattributes [ $ldap_result_emp_num ][ 0 ] : '' ;
$item [ 'lastname' ] = isset ( $ldapattributes [ $ldap_result_last_name ][ 0 ]) ? $ldapattributes [ $ldap_result_last_name ][ 0 ] : '' ;
$item [ 'firstname' ] = isset ( $ldapattributes [ $ldap_result_first_name ][ 0 ]) ? $ldapattributes [ $ldap_result_first_name ][ 0 ] : '' ;
$item [ 'email' ] = isset ( $ldapattributes [ $ldap_result_email ][ 0 ]) ? $ldapattributes [ $ldap_result_email ][ 0 ] : '' ;
$item [ 'telephone' ] = isset ( $ldapattributes [ $ldap_result_phone ][ 0 ]) ? $ldapattributes [ $ldap_result_phone ][ 0 ] : '' ;
$item [ 'jobtitle' ] = isset ( $ldapattributes [ $ldap_result_jobtitle ][ 0 ]) ? $ldapattributes [ $ldap_result_jobtitle ][ 0 ] : '' ;
$item [ 'country' ] = isset ( $ldapattributes [ $ldap_result_country ][ 0 ]) ? $ldapattributes [ $ldap_result_country ][ 0 ] : '' ;
$item [ 'department' ] = isset ( $ldapattributes [ $ldap_result_dept ][ 0 ]) ? $ldapattributes [ $ldap_result_dept ][ 0 ] : '' ;
2020-11-30 17:11:44 -08:00
2021-06-10 13:15:52 -07:00
return $item ;
2020-11-30 17:11:44 -08:00
}
/**
* Create user from LDAP attributes
*
* @ author [ A . Gianotto ] [ < snipe @ snipe . net > ]
* @ since [ v3 . 0 ]
* @ param $ldapatttibutes
* @ return array | bool
*/
2021-06-10 13:15:52 -07:00
public static function createUserFromLdap ( $ldapatttibutes )
2020-11-30 17:11:44 -08:00
{
2021-06-10 13:15:52 -07:00
$item = self :: parseAndMapLdapAttributes ( $ldapatttibutes );
2020-11-30 17:11:44 -08:00
// Create user from LDAP data
2021-06-10 13:15:52 -07:00
if ( ! empty ( $item [ 'username' ])) {
2020-11-30 17:11:44 -08:00
$user = new User ;
2021-06-10 13:15:52 -07:00
$user -> first_name = $item [ 'firstname' ];
$user -> last_name = $item [ 'lastname' ];
$user -> username = $item [ 'username' ];
$user -> email = $item [ 'email' ];
2020-11-30 17:11:44 -08:00
2021-06-10 13:15:52 -07:00
if ( Setting :: getSettings () -> ldap_pw_sync == '1' ) {
$user -> password = bcrypt ( Input :: get ( 'password' ));
2020-11-30 17:11:44 -08:00
} else {
2021-06-10 13:15:52 -07:00
$pass = substr ( str_shuffle ( '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ), 0 , 25 );
2020-11-30 17:11:44 -08:00
$user -> password = bcrypt ( $pass );
}
$user -> activated = 1 ;
$user -> ldap_import = 1 ;
$user -> notes = 'Imported on first login from LDAP' ;
if ( $user -> save ()) {
return $user ;
} else {
LOG :: debug ( 'Could not create user.' . $user -> getErrors ());
2021-06-10 13:15:52 -07:00
throw new Exception ( 'Could not create user: ' . $user -> getErrors ());
2020-11-30 17:11:44 -08:00
}
}
return false ;
}
/**
* Searches LDAP
*
* @ author [ A . Gianotto ] [ < snipe @ snipe . net > ]
* @ since [ v3 . 0 ]
* @ param $ldapatttibutes
* @ param $base_dn
* @ return array | bool
*/
2021-06-10 13:15:52 -07:00
public static function findLdapUsers ( $base_dn = null )
2020-11-30 17:11:44 -08:00
{
2021-06-10 13:15:52 -07:00
$ldapconn = self :: connectToLdap ();
$ldap_bind = self :: bindAdminToLdap ( $ldapconn );
2020-11-30 17:11:44 -08:00
// Default to global base DN if nothing else is provided.
if ( is_null ( $base_dn )) {
$base_dn = Setting :: getSettings () -> ldap_basedn ;
}
$filter = Setting :: getSettings () -> ldap_filter ;
// Set up LDAP pagination for very large databases
$page_size = 500 ;
$cookie = '' ;
2021-06-10 13:15:52 -07:00
$result_set = [];
2020-11-30 17:11:44 -08:00
$global_count = 0 ;
// Perform the search
do {
// Paginate (non-critical, if not supported by server)
2021-06-10 13:15:52 -07:00
if ( ! $ldap_paging = @ ldap_control_paged_result ( $ldapconn , $page_size , false , $cookie )) {
2020-11-30 17:11:44 -08:00
throw new Exception ( 'Problem with your LDAP connection. Try checking the Use TLS setting in Admin > Settings. ' );
}
if ( $filter != '' && substr ( $filter , 0 , 1 ) != '(' ) { // wrap parens around NON-EMPTY filters that DON'T have them, for back-compatibility with AdLdap2-based filters
$filter = " ( $filter ) " ;
2021-04-14 11:17:59 -07:00
} elseif ( $filter == '' ) {
2021-06-10 13:15:52 -07:00
$filter = '(cn=*)' ;
2020-11-30 17:11:44 -08:00
}
2021-04-14 11:17:59 -07:00
2020-11-30 17:11:44 -08:00
$search_results = ldap_search ( $ldapconn , $base_dn , $filter );
2021-06-10 13:15:52 -07:00
if ( ! $search_results ) {
2020-11-30 17:11:44 -08:00
return redirect () -> route ( 'users.index' ) -> with ( 'error' , trans ( 'admin/users/message.error.ldap_could_not_search' ) . ldap_error ( $ldapconn )); // FIXME this is never called in any routed context - only from the Artisan command. So this redirect will never work.
}
// Get results from page
$results = ldap_get_entries ( $ldapconn , $search_results );
2021-06-10 13:15:52 -07:00
if ( ! $results ) {
2020-11-30 17:11:44 -08:00
return redirect () -> route ( 'users.index' ) -> with ( 'error' , trans ( 'admin/users/message.error.ldap_could_not_get_entries' ) . ldap_error ( $ldapconn )); // FIXME this is never called in any routed context - only from the Artisan command. So this redirect will never work.
}
// Add results to result set
$global_count += $results [ 'count' ];
$result_set = array_merge ( $result_set , $results );
@ ldap_control_paged_result_response ( $ldapconn , $search_results , $cookie );
} while ( $cookie !== null && $cookie != '' );
// Clean up after search
$result_set [ 'count' ] = $global_count ;
$results = $result_set ;
@ ldap_control_paged_result ( $ldapconn , 0 );
return $results ;
}
}