2018-12-06 14:05:43 -08:00
< ? php
declare ( strict_types = 1 );
2019-01-10 13:20:43 -08:00
namespace App\Services ;
2018-12-06 14:05:43 -08:00
2019-03-13 20:12:03 -07:00
use Adldap\Adldap ;
use Adldap\Models\User as AdldapUser ;
use Adldap\Query\Paginator ;
2019-01-24 16:15:44 -08:00
use Adldap\Schemas\Schema ;
2019-01-15 14:05:47 -08:00
use App\Helpers\Helper ;
2019-03-13 20:12:03 -07:00
use App\Models\User ;
2018-12-06 14:05:43 -08:00
use Exception ;
use Illuminate\Support\Collection ;
use Illuminate\Support\Facades\Log ;
/**
* LDAP queries .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*/
class LdapAd extends LdapAdConfiguration
{
/**
* @ see https :// wdmsb . wordpress . com / 2014 / 12 / 03 / descriptions - of - active - directory - useraccountcontrol - value /
*/
const AD_USER_ACCOUNT_CONTROL_FLAGS = [ '512' , '544' , '66048' , '66080' , '262656' , '262688' , '328192' , '328224' ];
/**
* The LDAP results per page .
*/
const PAGE_SIZE = 500 ;
/**
* A base dn .
*
* @ var string
*/
public $baseDn = null ;
/**
* Adldap instance .
*
* @ var \Adldap\Adldap
*/
protected $ldap ;
/**
2019-01-10 13:20:43 -08:00
* Initialize LDAP from user settings
*
* @ since 5.0 . 0
*
2020-08-14 14:45:05 -07:00
* @ return void
2018-12-06 14:05:43 -08:00
*/
2020-08-14 14:45:05 -07:00
public function init ()
2018-12-06 14:05:43 -08:00
{
2019-01-10 13:20:43 -08:00
// Already initialized
2020-09-08 21:13:24 -07:00
if ( $this -> ldap ) {
2019-01-10 13:20:43 -08:00
return true ;
}
parent :: init ();
2018-12-11 21:01:11 -08:00
if ( $this -> isLdapEnabled ()) {
2020-08-26 12:25:10 -07:00
$this -> ldapConfig [ 'account_prefix' ] = $this -> ldapSettings [ 'ldap_auth_filter_query' ];
2020-03-24 14:27:14 -07:00
$this -> ldapConfig [ 'account_suffix' ] = ',' . $this -> ldapConfig [ 'base_dn' ];
2018-12-11 21:01:11 -08:00
$this -> ldap = new Adldap ();
$this -> ldap -> addProvider ( $this -> ldapConfig );
2019-01-10 13:20:43 -08:00
return true ;
2018-12-11 21:01:11 -08:00
}
2019-01-10 13:20:43 -08:00
return false ;
2018-12-06 14:05:43 -08:00
}
2020-08-14 14:45:05 -07:00
public function __construct () {
$this -> init ();
}
2019-01-10 13:20:43 -08:00
/**
2018-12-06 14:05:43 -08:00
* Create a user if they successfully login to the LDAP server .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*
* @ param string $username
* @ param string $password
*
* @ return \App\Models\User
*
* @ throws Exception
*/
public function ldapLogin ( string $username , string $password ) : User
{
2020-02-04 12:47:49 -08:00
if ( $this -> ldapSettings [ 'ad_append_domain' ]) {
$username .= '@' . $this -> ldapSettings [ 'ad_domain' ];
}
2020-03-24 14:27:14 -07:00
if ( $this -> ldap -> auth () -> attempt ( $username , $password , true ) === false ) {
2018-12-06 14:05:43 -08:00
throw new Exception ( 'Unable to validate user credentials!' );
2020-03-24 14:27:14 -07:00
}
2018-12-06 14:05:43 -08:00
// Should we sync the logged in user
2019-01-24 16:15:44 -08:00
Log :: debug ( 'Attempting to find user in LDAP directory' );
$record = $this -> ldap -> search () -> findBy ( $this -> ldapSettings [ 'ldap_username_field' ], $username );
if ( $record ) {
if ( $this -> isLdapSync ( $record )) {
$this -> syncUserLdapLogin ( $record , $password );
2018-12-06 14:05:43 -08:00
}
2019-01-24 16:15:44 -08:00
}
else {
2019-01-15 14:05:47 -08:00
throw new Exception ( 'Unable to find user in LDAP directory!' );
2018-12-06 14:05:43 -08:00
}
return User :: where ( 'username' , $username )
-> whereNull ( 'deleted_at' ) -> where ( 'ldap_import' , '=' , 1 )
-> where ( 'activated' , '=' , '1' ) -> first ();
}
/**
* Set the user information based on the LDAP settings .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*
* @ param \Adldap\Models\User $user
* @ param null | Collection $defaultLocation
* @ param null | Collection $mappedLocations
*
* @ return null | \App\Models\User
*/
public function processUser ( AdldapUser $user , ? Collection $defaultLocation = null , ? Collection $mappedLocations = null ) : ? User
{
// Only sync active users
2019-01-15 14:05:47 -08:00
if ( ! $user ) {
return null ;
2018-12-06 14:05:43 -08:00
}
2019-01-15 14:05:47 -08:00
$snipeUser = [];
$snipeUser [ 'username' ] = $user -> { $this -> ldapSettings [ 'ldap_username_field' ]}[ 0 ] ? ? '' ;
$snipeUser [ 'employee_number' ] = $user -> { $this -> ldapSettings [ 'ldap_emp_num' ]}[ 0 ] ? ? '' ;
$snipeUser [ 'lastname' ] = $user -> { $this -> ldapSettings [ 'ldap_lname_field' ]}[ 0 ] ? ? '' ;
$snipeUser [ 'firstname' ] = $user -> { $this -> ldapSettings [ 'ldap_fname_field' ]}[ 0 ] ? ? '' ;
$snipeUser [ 'email' ] = $user -> { $this -> ldapSettings [ 'ldap_email' ]}[ 0 ] ? ? '' ;
2019-01-24 16:15:44 -08:00
$snipeUser [ 'title' ] = $user -> getTitle () ? ? '' ;
$snipeUser [ 'telephonenumber' ] = $user -> getTelephoneNumber () ? ? '' ;
2019-01-15 14:05:47 -08:00
$snipeUser [ 'location_id' ] = $this -> getLocationId ( $user , $defaultLocation , $mappedLocations );
$snipeUser [ 'activated' ] = $this -> getActiveStatus ( $user );
return $this -> setUserModel ( $snipeUser );
2018-12-06 14:05:43 -08:00
}
/**
* Set the User model information .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*
* @ param array $userInfo The user info to save to the database
*
* @ return \App\Models\User
*/
public function setUserModel ( array $userInfo ) : User
{
// If the username exists, return the user object, otherwise create a new user object
$user = User :: firstOrNew ([
'username' => $userInfo [ 'username' ],
]);
$user -> username = $user -> username ? ? trim ( $userInfo [ 'username' ]);
2019-01-15 14:05:47 -08:00
$user -> password = $user -> password ? ? Helper :: generateEncyrptedPassword ();
2018-12-06 14:05:43 -08:00
$user -> first_name = trim ( $userInfo [ 'firstname' ]);
$user -> last_name = trim ( $userInfo [ 'lastname' ]);
$user -> email = trim ( $userInfo [ 'email' ]);
$user -> employee_num = trim ( $userInfo [ 'employee_number' ]);
2019-01-24 16:15:44 -08:00
$user -> jobtitle = trim ( $userInfo [ 'title' ]);
$user -> phone = trim ( $userInfo [ 'telephonenumber' ]);
2018-12-06 14:05:43 -08:00
$user -> activated = $userInfo [ 'activated' ];
$user -> location_id = $userInfo [ 'location_id' ];
$user -> notes = 'Imported from LDAP' ;
$user -> ldap_import = 1 ;
return $user ;
}
/**
* Sync a user who has logged in by LDAP .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*
* @ param \Adldap\Models\User $record
* @ param string $password
*
* @ throws Exception
*/
private function syncUserLdapLogin ( AdldapUser $record , string $password ) : void
{
$user = $this -> processUser ( $record );
if ( is_null ( $user -> last_login )) {
$user -> notes = 'Imported on first login from LDAP2' ;
}
if ( $this -> ldapSettings [ 'ldap_pw_sync' ]) {
Log :: debug ( 'Syncing users password with LDAP directory.' );
$user -> password = bcrypt ( $password );
}
if ( ! $user -> save ()) {
Log :: debug ( 'Could not save user. ' . $user -> getErrors ());
throw new Exception ( 'Could not save user: ' . $user -> getErrors ());
}
}
/**
* Check to see if we should sync the user with the LDAP directory .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*
* @ param \Adldap\Models\User $user
*
* @ return bool
*/
private function isLdapSync ( AdldapUser $user ) : bool
{
2019-01-24 16:15:44 -08:00
return ( false == $this -> ldapSettings [ 'ldap_active_flag' ])
2018-12-06 14:05:43 -08:00
|| ( 'true' == strtolower ( $user -> { $this -> ldapSettings [ 'ldap_active_flag' ]}[ 0 ]));
}
/**
* Set the active status of the user .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*
* @ param \Adldap\Models\User $user
*
* @ return int
*/
private function getActiveStatus ( AdldapUser $user ) : int
{
$activeStatus = 0 ;
/*
* Check to see if we are connected to an AD server
* if so , check the Active Directory User Account Control Flags
*/
2019-01-24 16:15:44 -08:00
if ( $user -> hasAttribute ( $user -> getSchema () -> userAccountControl ())) {
2018-12-06 14:05:43 -08:00
$activeStatus = ( in_array ( $user -> getUserAccountControl (), self :: AD_USER_ACCOUNT_CONTROL_FLAGS )) ? 1 : 0 ;
} else {
// If there is no activated flag, assume this is handled via the OU and activate the users
2019-01-24 16:15:44 -08:00
if ( false == $this -> ldapSettings [ 'ldap_active_flag' ]) {
2018-12-06 14:05:43 -08:00
$activeStatus = 1 ;
}
}
return $activeStatus ;
}
/**
* Get a default selected location , or a OU mapped location if available .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*
2019-01-10 13:20:43 -08:00
* @ param \Adldap\Models\User $user
2018-12-06 14:05:43 -08:00
* @ param Collection | null $defaultLocation
* @ param Collection | null $mappedLocations
*
* @ return null | int
*/
private function getLocationId ( AdldapUser $user , ? Collection $defaultLocation , ? Collection $mappedLocations ) : ? int
{
$locationId = null ;
// Set the users default locations, if set
if ( $defaultLocation ) {
$locationId = $defaultLocation -> keys () -> first ();
}
// Check to see if the user is in a mapped location
if ( $mappedLocations ) {
$location = $mappedLocations -> filter ( function ( $value , $key ) use ( $user ) {
2020-10-06 18:31:06 -07:00
//if ($user->inOu($value)) { // <----- *THIS* seems not to be working, and it seems more 'intelligent' - but it's literally just a strpos() call, and it doesn't work quite right against plain strings
$user_ou = substr ( $user -> getDn (), - strlen ( $value )); // get the LAST chars of the user's DN, the count of those chars being the length of the thing we're checking against
if ( strcasecmp ( $user_ou , $value ) === 0 ) { // case *IN*sensitive comparision - some people say OU=blah, some say ou=blah. returns 0 when strings are identical (which is a little odd, yeah)
return $key ; // WARNING: we are doing a 'filter' - not a regular for-loop. So the answer(s) get "return"ed into the $location array
2018-12-06 14:05:43 -08:00
}
});
if ( $location -> count () > 0 ) {
2020-10-06 18:31:06 -07:00
$locationId = $location -> keys () -> first (); // from the returned $location array from the ->filter() method above, we return the first match - there should be only one
2018-12-06 14:05:43 -08:00
}
}
return $locationId ;
}
/**
* Get the base dn for the query .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*
* @ return string
*/
private function getBaseDn () : string
{
if ( ! is_null ( $this -> baseDn )) {
return $this -> baseDn ;
}
return $this -> ldapSettings [ 'ldap_basedn' ];
}
/**
* Format the ldap filter if needed .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*
* @ return null | string
*/
private function getFilter () : ? string
{
$filter = $this -> ldapSettings [ 'ldap_filter' ];
2019-05-23 19:59:27 -07:00
if ( ! $filter ) {
2018-12-06 14:05:43 -08:00
return null ;
}
// Add surrounding parentheses as needed
$paren = mb_substr ( $filter , 0 , 1 , 'utf-8' );
if ( '(' !== $paren ) {
return '(' . $filter . ')' ;
}
return $filter ;
}
/**
* Get the selected fields to return
* This should help with memory on large result sets as we are not returning all fields .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*
* @ return array
*/
private function getSelectedFields () : array
{
2019-01-24 16:15:44 -08:00
/** @var Schema $schema */
$schema = new $this -> ldapConfig [ 'schema' ];
2018-12-06 14:05:43 -08:00
return [
$this -> ldapSettings [ 'ldap_username_field' ],
$this -> ldapSettings [ 'ldap_fname_field' ],
$this -> ldapSettings [ 'ldap_lname_field' ],
$this -> ldapSettings [ 'ldap_email' ],
$this -> ldapSettings [ 'ldap_emp_num' ],
2019-01-15 14:05:47 -08:00
$this -> ldapSettings [ 'ldap_active_flag' ],
2019-01-24 16:15:44 -08:00
$schema -> memberOf (),
$schema -> userAccountControl (),
$schema -> title (),
$schema -> telephone (),
2018-12-06 14:05:43 -08:00
];
}
/**
* Test the bind user connection .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
2020-04-23 14:41:37 -07:00
* @ throws \Exception
2018-12-06 14:05:43 -08:00
* @ since 5.0 . 0
*/
public function testLdapAdBindConnection () : void
{
try {
2020-08-14 14:45:05 -07:00
$this -> ldap -> search () -> ous () -> get () -> count (); //it's saying this is null?
2018-12-06 14:05:43 -08:00
} catch ( Exception $th ) {
Log :: error ( $th -> getMessage ());
throw new Exception ( 'Unable to search LDAP directory!' );
}
}
/**
* Test the user can connect to the LDAP server .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
2020-04-23 14:41:37 -07:00
* @ throws \Exception
2018-12-06 14:05:43 -08:00
* @ since 5.0 . 0
*/
public function testLdapAdUserConnection () : void
{
try {
2020-08-14 14:45:05 -07:00
$this -> ldap -> connect (); //uh, this doesn't seem to exist :/
2018-12-06 14:05:43 -08:00
} catch ( \Adldap\Auth\BindException $e ) {
Log :: error ( $e );
throw new Exception ( 'Unable to connect to LDAP directory!' );
}
}
/**
* Test the LDAP configuration by returning up to 10 users .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*
* @ return Collection
*/
public function testUserImportSync () : Collection
{
$testUsers = collect ( $this -> getLdapUsers () -> getResults ()) -> chunk ( 10 ) -> first ();
if ( $testUsers ) {
return $testUsers -> map ( function ( $item ) {
return ( object ) [
'username' => $item -> { $this -> ldapSettings [ 'ldap_username_field' ]}[ 0 ] ? ? null ,
'employee_number' => $item -> { $this -> ldapSettings [ 'ldap_emp_num' ]}[ 0 ] ? ? null ,
'lastname' => $item -> { $this -> ldapSettings [ 'ldap_lname_field' ]}[ 0 ] ? ? null ,
'firstname' => $item -> { $this -> ldapSettings [ 'ldap_fname_field' ]}[ 0 ] ? ? null ,
'email' => $item -> { $this -> ldapSettings [ 'ldap_email' ]}[ 0 ] ? ? null ,
];
});
}
return collect ();
}
/**
* Query the LDAP server to get the users to process and return a page set .
*
* @ author Wes Hulette < jwhulette @ gmail . com >
*
* @ since 5.0 . 0
*
* @ return \Adldap\Query\Paginator
*/
2019-06-14 09:54:09 -07:00
public function getLdapUsers () : Paginator
2018-12-06 14:05:43 -08:00
{
2020-08-14 14:45:05 -07:00
$search = $this -> ldap -> search () -> users () -> in ( $this -> getBaseDn ()); //this looks wrong; we should instead have a passable parameter that does this, and use this as a 'sane' default, yeah?
2018-12-06 14:05:43 -08:00
$filter = $this -> getFilter ();
if ( ! is_null ( $filter )) {
$search = $search -> rawFilter ( $filter );
}
return $search -> select ( $this -> getSelectedFields ())
2019-06-14 09:54:09 -07:00
-> paginate ( self :: PAGE_SIZE );
2018-12-06 14:05:43 -08:00
}
}